MicroPython - Lambda Functions

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:

What are Lambda Functions?

A lambda function is a special type of function that doesn't have a function name. The lambda keyword is used instead of def to create a lambda function.

Syntax


lambda arguments : expression

Parameters:
  arguments: Arguments that are passed
              to the expression.

  expression: Expression that is evaluated.

Example:
   lambda a, b, c: a + b + c   

          

A Lambda function is an expression, not a statement. It can have any number of arguments but consists of only one expression. This means that the use of constructs such as loop and conditional statements are expressively forbidden.

A lambda expression returns a value (a new function) that can optionally be assigned a name.


Sum = lambda a, b, c: a + b + c
print(Sum(1, 2, 3))  ⇒ 6

          

Lambda functions will accept keyword arguments and default arguments just as a normal MicroPython function. Consider the following example for a lambda function that evaluates a binomial equation:

# Evaluate a binomial equation
# Keyword argument example
y = lambda x, a, b, c : a*x*x + b*x +c
y(5, 2, 3, 4)  ⇒ 69
y(a=2, c=4, x=5, b=3)  ⇒ 69

# Default argument example
# In this case c=0 by default.
y = lambda x, a, b, c=0 : a*x*x + b*x +c
y(5, 2, 3)  ⇒ 65 
          

The keyword argument example shows how the arguments can be passed to the lambda function in any order provided the keyword is used.

The default argument example has the parameter c assigned a default value in the lambda definition. This default value is used (as in this example) if the equivalent argument is not passed when the lambda function is invoked.

Lambda Scope Rules

The code in a lambda body also follows the same scope lookup rules as code inside a def function. Lambda expressions inside a function have access to names that appear inside the enclosing function.

This is best shown by example.

Example 1

# Demonstrates lambda function scope rules.
# A lambda function defined inside
# a 'def' function has access to all
# the locally defined variables.

import math

def Sine():
    # Calculates the sin() for an angle
    # given in degrees.
    # The sin() function from the 'math'
    # module expects an angle given in
    # radians. The radians must be
    # converted to degrees.
    DegToRad = 1/57.2958
    SinDeg = lambda deg: math.sin(deg * DegToRad)
    return SinDeg
    
mySine = Sine()
# Define an angle in degrees
myAngle = 45
print('Sine(' + str(myAngle) + ') =', mySine(45))
          
Output:

Sin(45) = 0.7071066
          

In Example 1 the conversion factor DegToRad is local to the function Sin(). However as the lambda expression SinDeg is also enclosed in Sin(), it is able to access DegToRad.

Nested Lambda Functions

A Lambda can be nested in another Lambda. Similar to nested def functions, the nested lambda has access to the variables in the enclosing lambda.

Example 2

# Demonstrate nesting a lambda
# inside another lambda.

# Lambda function used to create
# a binomial equation.
# Inner lambda function evaluates
# the binomial equation.
binomial = (lambda a, b, c: (lambda x: a*x* + b*x +c))

# Define the binomial equation
a, b, c = 1.9, 20.8, 35
y = binomial(a, b, c)

# Test the lambda to solve
# the binomial equation
# for x = 0 to 9.
print ('Binomial equation is:')
print('y =', a, end='')
print('*x*x + ', b, end='')
print('*x + ', c)
print()
for x in range(10):
    print('x =', x, ', y =', y(x))

Output:

Binomial equation is:
y = 1.9*x*x +  20.8*x +  35

x = 0 , y = 35.0
x = 1 , y = 74.52
x = 2 , y = 193.08
x = 3 , y = 390.68
x = 4 , y = 667.3199
x = 5 , y = 1023.0
x = 6 , y = 1457.72
x = 7 , y = 1971.48
x = 8 , y = 2564.28
x = 9 , y = 3236.12

In Example 2 shows the creation of a binomial equation that is evaluated; all in a single line of code. The parent lambda function takes the three standard binomial constants as parameters. The body of this parent (outer) lambda contains a nested lambda function that has a single parameter, the binomial x value. This inner lambda function then calculates a value for the binomial.

The inner lambda is able to make this calculation because it has access to the parameters of the enclosing lambda i.e. the binomial constants a, b and c.

The program outputs a series of 'x' and 'y' pairs for the binomial equation that could then be sent to a plotting function.

In general, nesting lambda functions can produce convoluted code that in the interest of readability is often best avoided.

Why Use Lambda Functions?

Lambda is handy as a sort of function shorthand that allows the embedding of a function's definition within the code that uses it. They are entirely optional as a def function could be used instead.

Example 3 below shows how it is possible to use simple selection logic in a lambda function with the ternary selection operator: x if y else z.

Example 3

# Demonstrate use of a lambda using simple
# selection logic.

# This program generates a random list of
# floats. The list of floats are sorted
# using the simple bubble sort algorithm.

# Generate random float numbers.
from random import seed, uniform

# Initialise random number generator
# from system clock.
from utime import ticks_us
seed(ticks_us())

def PrintList(L, ItemsPerLine):
    # Prints all items of a list.
    # Max number of items printed
    # per line is given by 'ItemsPerLine'.
    count = 1
    for item in L:
        print(item, end='')
        count += 1
        if count > ItemsPerLine: 
            print()
            count = 1
        else:
            print(',', end = '  ')
            
def BubbleSort(L):
    # Sorts a list of numbers using
    # a simple bubble sort algorithm.
    # Work through every number in
    # the list.
    # For a given number, if the next
    # number in the list is smaller,
    # then swap the two numbers around.
    # Continue cycling thorough the list
    # till no swaps are necessary.
    # The list is now sorted in
    # ascending order.
    finished = False
    # The following lambda returns True if
    # the two numbers need swapping.
    toSwap = lambda a, b: True if a > b else False
    while not finished:
        count = 0
        for i in range(0, len(L)-1):
            if toSwap(L[i], L[i+1]):
                # Swap numbers
                temp = L[i]
                L[i] = L[i+1]
                L[i+1] = temp
                count += 1
       # Stop when complete cycle through
        # the list results in no
        # swaps being done.
        if count == 0: finished = True
    return L

# Random number envelope.
lRand, uRand = 1, 100
# Number of floats to randomly generate.
num = 24

# Generate an unsorted list of
# random floats and print out.
List = []
for count in range(0, num):
    List.append(uniform(lRand, uRand))
print('Unsorted list')
PrintList(List, 4)    

# Sort the list and print out.
print('\nSorted list')
List = BubbleSort(List)
PrintList(List, 4)

Output:

Unsorted list
80.0687,  83.34172,  38.82649,  96.16801
99.69688,  14.61374,  71.28607,  59.77497
51.72344,  94.50739,  67.13476,  5.261813
32.32739,  72.83759,  85.39958,  21.64218
50.84931,  33.28698,  17.59793,  13.40086
37.22881,  97.16938,  98.06762,  1.128273

Sorted list
1.128273,  5.261813,  13.40086,  14.61374
17.59793,  21.64218,  32.32739,  33.28698
37.22881,  38.82649,  50.84931,  51.72344
59.77497,  67.13476,  71.28607,  72.83759
80.0687,  83.34172,  85.39958,  94.50739
96.16801,  97.16938,  98.06762,  99.69688

In this case (Example 3) the use of the lambda toSwap is trivial. However using it to determine whether two elements need to be swapped adds to the ease of code readability.

Lambda functions are commonly used to code jump tables. These are lists or dictionaries of actions to be performed on demand. Consider the following example:

Example 4

# Demonstrates a list to store
# lambda functions. The lambdas
# can be accessed by list index.

# Diagonal length, surface area
# and volume can be calculated for
# a cube if the side measurement
# is known. By definition all side
# measurements of a cube are identical.

# Define lambda functions to describe a cube.
# Store the lambdas in a list.
cube = [lambda side: print('My cube has side length of', side),
        lambda side: print('Diagonal length =', side * pow(3, 1/3)),
        lambda side: print('Surface area =', 6 * side * side),
        lambda side: print('Volume =', side * side * side)]

# Test the lambda functions
myCube = 5.93 # Side measurement of the cube.
for calc in cube:
    calc(myCube)
    calculation(myCube)

Output:

My cube has side length of 5.93
Diagonal length = 8.55254
Surface area = 210.9894
Volume = 208.5278

Example 4 uses a list of lambda functions to return a set of statistics on a cube given the length of one side (all sides of a cube have the same length measurement by definition).

This is an elegant solution that is succinct and easy to maintain. The list of lambda functions can be easily edited, added to, and so on without affecting the other parts of the program.

Example 5 uses a dictionary of lambdas as a jump table

Example 5

# Demonstrate use of lambda functions
# in a jump table.

# This program tests the execution time to
# sum up numbers from one (1) to a given value.
# The sum and execution time is printed.
# This process is carried out for all
# in the list 'test'.

from utime import ticks_ms, ticks_us, ticks_diff
# utime module provides timing functions for
# the micro:bit.

# ticks_ms() returns the system time in mSec.
# ticks_us() returns the system time in uSec.
# ticks_diff(stop, start) returns elapsed time
# between 'start' and 'stop'.

# 'start' and 'stop' should be obtained by
# calling ticks_ms() or ticks_us().

# Sums all numbers from 1 to 'x'
def adder(x):
    sum = 0
    for value in range(1, x+1):
        sum += value
    return sum

# Dictionary that defines a jump table of lambdas.       
Fn = {'timer': (lambda unit: ticks_ms() if unit == 'mSec' else ticks_us()),
      'time_diff': (lambda start, stop: ticks_diff(stop, start)),
      'header': (lambda x: print('Adding up the first', x, 'numbers')),
      'result': (lambda sum: print('The sum is', sum))}
          
test = [10, 100, 1000, 10000, 100000, 1000000]

for x in test:
    # Set the timing units (mSec or uSec) determined
    # by how many numbers to be summed.
    unit = 'mSec' if x >= 1000 else 'uSec'
    Fn['header'](x)
    # Get start time
    start = Fn['timer'](unit)
    sum = adder(x)
    # Get finish time
    stop = Fn['timer'](unit)
    Fn['result'](sum)
    print('This took', Fn['time_diff'](start, stop), unit, '\n')

Output:

Adding up the first 10 numbers
The sum is 55
This took 250 uSec 

Adding up the first 100 numbers
The sum is 5050
This took 1766 uSec 

Adding up the first 1000 numbers
The sum is 500500
This took 11 mSec 

Adding up the first 10000 numbers
The sum is 50005000
This took 108 mSec 

Adding up the first 100000 numbers
The sum is 5000050000
This took 2097 mSec 

Adding up the first 1000000 numbers
The sum is 500000500000
This took 28896 mSec 

Example 5 stores a jump list of lambda functions in the dictionary Fn. Indexing by key fetches the required function and parentheses forces the fetched function to be called.

Using lambdas in this case improves the readability by removing functional detail from the main program body.

As always, these lambdas could have been coded as def functions:


def timer(unit): 
  ticks_ms() if unit == 'mSec' else ticks_us()

def time_diff(start, stop):
  ticks_diff(stop, start)

def header(x):
  print('Adding up the first', x, 'numbers')

def result(sum):
  print('The sum is', sum)