Functions as Data

What def really does

Download: file
def function_name (parameter_name_1, ..., parameter_name_n):
    statement
    ...
    statement

The def keyword takes the body of the function definition (everything indented beneath the line starting with def) and packages it into a function object that can be evaluated later. It then associates that function object with the function_name you provided. It also takes the list of parameter names along with any default values and stores them in the function object.

Download: file
>>> def the_answer():
... return 42
...
>>> the_answer()
42
>>> the_answer
<function>
>>> f = the_answer
>>> f()
42

Notice that if we type the name of the function rather than a call to the function at the REPL prompt, the system tells us that it is a function. Furthermore, notice that we can assign the value of a function (not it's result) to a variable, then use that to call the function.

Creating a function within a function

Remember when we talked about scope: the part of the code in which a name is known and has a value? It so happens that the body of a function is a scope. Things like the function's parameters live in that scope. So do local variables you create (in Python you create local variables simply by assigning a value to a name within the scope). So do functions you define in that scope. Yes, you can create functions that are local to a function.

Here is a function to find square roots. It uses several helper functions. Since these helper functions are specific to the sqrt function, we define them inside it.

Download: file
    def sqrt(x):

    def square(a):
        return a * a
        
    def good_enough(g):
        return abs(square(g) - x) < 0.001
        
    def average(a, b):
        return (a + b) / 2
        
    def improve(g):
        return average(g, x / g)
        
    guess = 1.0
    while not good_enough(guess):
        guess = improve(guess)
    return guess
  

If you take a minute to understand this code, you might think "Why can't you just write it like this:"

Download: file
def sqrt(x):
    guess = 1.0
    while abs(guess * guess - x) > 0.001:
        guess = (guess + x / guess) / 2
    return guess

You could. It works the same. And if you were constrained for space, like on a SAMD21, it could be worth the denser code. But this series is about things you can do on the SAMD51 MCU. You have room to write nicer, cleaner, better code. The first version is easier to read,  understand, and tweak as required. That's worth some space, if you can afford it. The first version is simply the second decomposed into meaningful chunks and given names that explain what each one does. Because they are defined inside the sqrt function, they don't pollute the namespace with single use functions.

Returning functions

Functions in Python are first class data. One thing that means is that they can be created in another function and returned. That's pretty cool. As an example, consider the following:

Download: file
def make_adder(x):
    def adder(a):
    	return a + x
    return adder

The function make_adder defines and returns a function that has one parameter and returns the sum of it's argument and the argument to make_adder when the returned function was defined. Whew! Now we can try it out.

Download: file
>>> inc = make_adder(1)
>>> inc
<closure>
>>> inc(3)
4
>>> inc(5)
6

That's interesting. The type of the returned function isn't function, it's closure. A closure is the combination of a function and the scope in which it was defined. In this example, the returned function refers to its creator function's parameter x. When the function is executed later (and is a different scope) it still has a hold of the value that x had when it was created: 1 in this case.

We can save the returned closure in a variable (we called it inc) and call it later just like a regularly defined function. Given the above we can now do:

Download: file
>>> dec = make_adder(-1)
>>> dec(1)
0
>>> dec(5)
4
>>> inc(3)
4

The dec function is totally separate from inc, which continues to work as before. Each time a new function is created and returned by make_adder it has a different copy of the scope, and so its own value for x.

Functions as Arguments

Let's reconsider the square root function:

Download: file
def sqrt(x):

    def square(a):
        return a * a
        
    def good_enough(g):
        return abs(square(g) - x) < 0.001
        
    def average(a, b):
        return (a + b) / 2
        
    def improve(g):
        return average(g, x / g)
        
    guess = 1.0
    while not good_enough(guess):
        guess = improve(guess)
    return guess

At the core of this is a general algorithm for finding a solution:

Download: file
while not good_enough(guess): 
   guess = improve(guess)

In English this is "While your guess isn't good enough, make a better guess."

The algorithm doesn't really care what good_enough and improve mean. Because functions in Python are first class data, they can not only be returned, but also used as arguments. So we can write a general solver:

Download: file
def solver(good_enough, improve, initial_guess):
    def solve(x):
        guess = initial_guess
        while not good_enough(x, guess):
            guess = improve(x, guess)
        return guess
    return solve

Now we can use this to create a square root solver given the two functions and an initial guess:

Download: file
def sqrt_good_enough(x, guess):
    def square(a):
        return a * a
    return abs(square(guess) - x) < 0.001

def sqrt_improve(x, guess):
    def average(a, b):
        return (a + b) / 2
    return average(guess, x / guess)

Putting it all together:

Download: file
sqrt = solver(sqrt_good_enough, sqrt_improve, 1.0)
>>> sqrt(25)
5.00002
This guide was first published on Aug 08, 2018. It was last updated on Aug 08, 2018. This page (Functions as Data) was last updated on Sep 15, 2019.