MicroPython - User Defined Functions Part 2

Contents

Introduction

MicroPython is a “slim” version of Python specifically designed with a small footprint to efficiently run on memory constrained microcontrollers.

MicroPython doesn't provide anywhere near the complete complement of Python functionality. However the subset that is implemented is useful, fit-for-purpose and mostly all that is required.

All examples in this article are original and have been tested on a micro:bit for correctness using the Mu Editor.

Other MicroPython articles in this series that the reader might find relevant to the topic covered in this posting include:

Passing functions as arguments

Functions are first class objects in MicroPython. This means that functions can be passed as arguments to other functions.

When a function is passed as an argument to another function it is referred to as a callback function. It is expected that at some point this callback function will be called.

Example 1

# Demonstrate the use of functions
# as first class objects.

# MicroPython functions are first class
# objects because they can be passed as
# arguments into other functions.

def say_hello_world(name):
    return name + ', the world says hello'

def say_hello_universe(name):
    return name + ', the universe says hello'

def greetings(name, greeter_func):
    # The greeter_func parameter is 
    # the name of another function.
    return greeter_func(name)
    
print(say_hello_world('Dora'))
print(say_hello_universe('Jack'))
print(greetings('Joe', say_hello_world))

          
Output:

Dora, the world says hello
Jack, the universe says hello
Joe, the world says hello

          

The first two print() statements make direct calls to the functions say_hello_world() and say_hello_universe() respectively.

The third print() statement makes an indirect call to the say_hello_world() when this function is passed as a parameter to the greetings() function.

This is a very powerful language feature since it means that the decision of which function will operate on a given piece of data can be made at runtime.

In the next example a random choice is made as to which function is passed to the greeter() function.

Example 2

# Demonstrate the use of functions
# as first class objects.

# Random number generator
from random import seed, randint

# Seed the random number generator
# from the system clock.
from utime import ticks_ms
seed(ticks_ms())

def say_hello(name):
    return 'Hello ' + name

def say_hello_world(name):
    return name + ', the world says hello'

def say_hello_universe(name):
    return name + ', the universe says hello'

def greetings(name, greeter_func):
    # The greeter_func parameter is
    # the name of another function.
    return greeter_func(name)

# List of all greeting functions available.
greeter_list = [say_hello,
                say_hello_world,
                say_hello_universe]
                

# List of names.
names_list = ['James', 'Joe', 'Karen']

# Randomly select a greeting
# for each name in the list.
for name in names_list:
    # Get a random number
    # between 0 and 2 inclusive.
    random_num = randint(0, 2)
    # Using the random number,
    # select the greeting function to use.
    greeter_to_use = greeter_list[random_num] 
    # Now, lets send the greeting.
    print(greetings(name, greeter_to_use))

          
Output:

James, the world says hello
Joe, the world says hello
Karen, the universe says hello

          

This time the function that greetings() calls is selected randomly at runtime. Functions can be elements in any compatible data structure. Note in the above example how the list greeter_list is used to record the available functions.

The Example 2 could be modified to use an if-elif-else statement to achieve the same result.


# From the random number, select
# the greeting function to use.
if random_num == 0:
    print(say_hello('Joe'))
elif random_num == 1:
    print(say_hello_world('Joe'))
else:
    # final case, random_num == 2
    print(say_hello_universe('Joe'))

          

This is not as elegant as the original single-line solution; it is more verbose, not as flexible and more difficult to maintain.

The above two examples pass user defined functions as arguments to other functions. Built-in functions from the core MicroPython language and functions provided by standard modules may also be used as arguments to be passed into other functions.

Example 3

# Demonstrate passing a built-in or
# standard module function as an
# argument to another function.

from math import sin, asin

def myMath(fn, argument):
    # 'fn' is a valid function that
    # accepts a single numeric 'argument'.
    if isinstance(argument, (int, float)):
        #'argument' is numerical
        return fn(argument)
    else:
        # 'argument' is not numerical
        return 'not defined'

# Test myMath() function.
print('sin(5) =', myMath(sin, 5))
print('asin("Spam") =', myMath(asin, 'Spam'))
print('abs(-500) =', myMath(abs, -500))

          
Output:

sin(5) = -0.9589243
asin("Spam") = not defined
abs(-500) = 500

          

Inner Functions

A function may also have other functions defined within it. These child functions then become local to the parent function.

Example 4

# Demonstrate functions that are defined
# within another function. These are known
# as inner functions.

def convert_temp(temp, unit = 'F'):
    # Converts a temperature in Celsius
    # to Fahrenheit or Kelvin.
    # Parameters:
    # 'temp' is temperature in Celsius.
    # 'unit' is the conversion unit: 
    #   'F' or 'f' = Fahrenheit
    #   'K' or 'k' = Kelvin
    
    # Inner function
    def toFahrenheit(temp):
        # Converts Celsius to Fahrenheit
        return temp * 9/5 + 32
     
    # Inner function
    def toKelvin(temp):
        # Converts Celsius to Kelvin
        return temp + 273
    
    # Body of parent function
    # Check 'temp' is numerical.
    temp_OK = isinstance(temp, (int, float))
    # Check 'unit' is a string
    unit_OK = isinstance(unit, str)
    if temp_OK and unit_OK:
        Unit = unit.upper()
        if Unit == 'F':
            return toFahrenheit(temp)
        elif Unit == 'K':
            return toKelvin(temp)
        else:
            # Invalid arguments passed
            # to the function.
            return Nothing
 
# Test convert_temp() function. 
print('37 Celsius =',
      convert_temp(37, 'F'), 'Fahrenheit')
      
print('37 Celsius =',
      convert_temp(37, 'k'), 'Kelvin')
      
print('"Spam" =',
      convert_temp("Spam", 'F'), 'Fahrenheit\n')

# Calling one of the inner functions
# of convert_temp() will raise an exception.
print('Call inner function of convert_temp()')
print('37 Celsius =',
       toFahreheit(37), 'Fahrenheit')
          
Output:

37 Celsius = 98.6 Fahrenheit
37 Celsius = 310 Kelvin
"Spam" = None Fahrenheit

Attempting to call an inner function of convert_temp()
Traceback (most recent call last):
  File "main.py", line 38, in <module>
NameError: name 'toFahreheit' isn't defined
          

The inner functions can be defined in any order but must appear above any code that calls them within the parent function.

These inner functions are effectively private as any attempt to call them from outside the parent function will result in a NameError exception being raised. This is shown in Example 4.

Returning Functions from Functions

Just as a function argument can be another function, a function may also return a function. The following Example 5 is fairly straight forward.

Example 5

# Demonstrate a function returning
# another function.

def arithmetic(op):
    # This function returns a function
    # based on which arithmetic operation
    # is requested.
    # Valid operations ('op') are:
    # 'add', 'subtract', 'multiply' and 'divide'

    # Define a dictionary of
    # allowable operation.
    op_dict = {'add':add,
               'subtract':subtract,
               'multiply':multiply,
               'divide':divide}
    # Look up the requested operation
    # in the dictionary.
    if op in op_dict:
        return op_dict[op]
    else:
        return Nothing

# Implement the operations
def add(value1, value2):
    return value1 + value2

def subtract(value1, value2):
    return value1 - value2

def multiply(value1, value2):
    return value1 * value2

def divide(value1, value2):
    return value1 / value2

#Assign function names
adder = arithmetic('add')
subtractor = arithmetic('subtract')
multiplier = arithmetic('multiply')
divider = arithmetic('divide')

# Test the operation functions.
print('3 + 5 =', adder(3, 5))
print('3 - 5 =', subtractor(3, 5))
print('3 x 5 =', multiplier(3, 5))
print('3 / 5 =', divider(3, 5))
          

The user calls the arithmetic() function with an argument of 'add', 'subtract', 'multiply' or 'divide'. The arithmetic() function does a dictionary lookup and returns the actual function that will do the operation requested.

Output:

3 + 5 = 8
3 - 5 = -2
3 x 5 = 15
3 / 5 = 0.6
          

Directly calling a function that is defined inside another function from outside the outer (parent) function will generate an exception. The inner function (child) is essentially private to the parent function.

However it is possible for code external to the parent function to indirectly call the child function. This is possible if the parent return value is the child function. The following example illustrates this concept.

Example 6

# Demonstrate how an (inner) function defined
# inside another (outer) function can be
# called externally.

# Square root function
from math import sqrt

def hypotenuse():
    # Returns the hypotenuse of a triangle given
    # the length of the other two sides.
    
    def isPosNum(side):
       # Check that the side measurement
        # is a positive number.
        isNum = isinstance(side, (int, float))
        # Check side measurement is not zero.
        notZero = side > 0
        if (isNum and notZero):
            return True
        else:
            return False
            
    def calc_hypotenuse(s1, s2):
        #'s1' and 's2' are the two side
        # measurements of the triangle.
        if isPosNum(s1) and isPosNum(s2):
            return sqrt(s1*s1 + s2*s2)
        else:
            return 'Not defined'
        
    return calc_hypotenuse
 
# Test hypotenuse() function
h = hypotenuse()
print('Hypotenuse with sides 3 and 4:')
print(h(3, 4))
print()

# It is possible to use a single function call.
print('Hypotenuse with sides 5 and 6.97:')
print(hypotenuse()(5, 6.97))
print()

# Show that error checking works.
print('Hypotenuse with sides 8 and -9:')
print(hypotenuse()(8, -9))

Output:

Hypotenuse with sides 3 and 4:
5.0

Hypotenuse with sides 5 and 6.97:
8.577931

Hypotenuse with sides 8 and -9:
Not defined
          

In Example 6 the function hypotenuse() has another function calc_hypotenuse() defined within it that carries out the actual calculation of the hypotenuse length.

A direct call on this inner function from outside the hypotenuse() function would result in an exception being raised. However a call to hypotenuse() returns the calc_hypotenuse() function that can now be called to calculate the hypotenuse length.

While Example 6 is trivial, in general this technique of a parent function returning its child function can abstract away the details of how the function works and provides a more high-level interface to the user.