Pushing The Limits: 2.1 Higher Order Functions
Pushing The Limits: 2.1 Higher Order Functions
Pushing The Limits: 2.1 Higher Order Functions
Now that you know how to put a basic program together, we’re going to start pushing the
limits of what we can do. We’re going to learn a bit more Python in this chapter, but mostly
we’re going to do new things with the Python we already know. Often we’ll be doing things
that seem strange, maybe even unacceptable. It might not be clear at first that the sort
of things we’re going to try are actually acceptable things to do. But they’ll all work, and
in figuring out exactly how and why they work, we’ll get a better understanding of exactly
how your computer carries out a Python program that you’ve given it.
x = 1
31
CHAPTER 2. PUSHING THE LIMITS 32
y = square (3)
print ( x )
This code sets x equal to 1, but then calls square. and square sets its input variable, also
x, equal to 3. So it might seem like when x is printed at the end, it should be qual to 3.
Instead, a 1 is printed. The x inside the definition of square is a local variable and is in
some sense a different variable from the x that appears outside of the function.
This distinction is very convenient — you can use square without knowing what it calls
its input variable, and you don’t have to worry about it causing a problem elsewhere in
your code. You can even change the implementation of square so that it uses a different
variable name, and you don’t need to worry about programs that use it breaking. The
natural question then, which we avoided before, is what exactly the rule is that Python uses
to figure out which variables are referring to what values, and our goal here is to answer
that question.
Python stores variables and their associated values in a simple table called a frame.
When a program begins, there is only one frame, called the global frame. Whenever a new
variable is assigned a value, a row is created in the table that stores the variable name and
the value that goes with it. If the variable’s value is changed, that row is modified.
x = 7
from math import pi
y = x + pi
x = 9
Figure 2.1: Some simple code and the resulting global frame. Earlier in the computation,
the x row would have contained a value of 7.
This gets more complicated when a function is run. In order to keep local variables local,
each execution of a function gets its own local frame. When the function call happens, the
first thing the Python interpreter does is create a new local frame. It then sets all the input
variables of the function equal to the values that were passed in the function call. Only then
does it begin executing the function. As the function’s code is being run, variable look-ups
happen in the local frame, rather than the global frame. Once the function is finished, the
return value is passed back as the value of the call expression and the local frame used to
evaluate the function is deleted from the computer’s memory.
Figure 2.2 gives an example of what happens when a function is called. There are
a couple new features here. First, we have a function definition. A def statement is a
type of variable assignment — in this case, it sets the variable square equal to a function
description. We draw this variable assignment a little differently in our diagram. When a
a variable is set equal to a simple value like a number, Python can store that value directly
in the frame table. A function description, however, can be long and complicated, so it is
stored elsewhere in the computer’s memory, and what is saved in the variable table is a
reference that describes the location of that function description. We depict this with an
arrow. (In reality, the description of a function is a complex description that essentially
saves the entire code of the function. To simplify our diagrams, we just note that it is a
CHAPTER 2. PUSHING THE LIMITS 33
def square ( x ):
return x * x
x = 1
y = square (3)
def example ( x ):
z = x + y
return z
y = 4
z = 2
a = example (3)
Figure 2.3: Here y is accessed inside the example frame, and when not found the value from
the global frame is used instead. The assignment of a value to z is done inside the local
frame and does not affect the value of z in the global frame.
Figure 2.3 introduces even more complication. In this code, y is accessed from inside
the call to example, but there is no y in the local frame. In this case, Python will check the
CHAPTER 2. PUSHING THE LIMITS 34
global frame. In this case y is found there to have the value 4, so that is the value used. (If
no value is found in the global frame either, Python will report an error.)
Figure 2.3 also contains an in-function assignment to variable z, which previously existed
only in the global frame. Variable assignment always happens in the local frame. A new
local variable will be created — the global frame variable will not be modified.
We use the term environment to refer to the set of all frames that can be accessed in
a given situation. So the environment in which the the body of example is being executed
consists of two frames, first the local frame labeled “example” and second the global frame.
def square ( x ):
return x * x
def twice ( func , x ):
return func ( func ( x ))
a = twice ( square , 3)
When square is passed as an input to twice it is evaluated, and the function value
bound to square is bound to the input variable func as well. Therefore the global variable
square and the local variable func both refer to the same function. That function is then
executed twice, first on the input x then again on the result. Note that because func is called
twice, two separate local frames are created, one for each call. (These frames are labeled
“square” in our diagrams, because that is the intrinsic name of the function, meaning the
name under which it was first defined. Remember that the names of frames are arbitrary
and are only there to help humans understand — they don’t affect how the code behaves
CHAPTER 2. PUSHING THE LIMITS 35
in any way.) There are many different x variables, each potentially containing a separate
value. But because the of how the rules for variable binding and evaluation are constructed,
the code behaves in a reasonably intuitive way.
Now, twice is a simple function meant to show how functions can be used as input.
It’s not likely to be practically useful very often. To see a more realistic example, consider
the task of summing integers. We gave code for summing the numbers one through ten in
subsection 1.7.5, and we can modify that code to create a function that can give the sum
of all integers up to a chosen value.
def sum_ints ( n ):
i , total = 1 , 0
while i <= n :
total += i
i += 1
return total
Now, sum_ints and sum_squares aren’t identical, but they’re very similar. This means
we could instead create an abstraction representing the general process, reducing the need
for (almost) repeated code. Note that both of these functions sum an infinite sequence, and
that ith term of the series is a function of i. We can write a function that sums arbitrary
series by taking this function as input.
def summation (n , term ):
i , total = 1 , 0
while i <= n :
total += term ( i )
i += 1
return total
Now, given that we have square defined already, we could sum the first 10 squares by
calling summation(10, square), and this might remove the need to define sum_squares at
all. But if we did want to define sum_squares, we could do it very easily.
def sum_squares ( n ):
return summation (n , square )
Now, identity seems like a very silly function. We would never bother to define it just for
its own sake. Nevertheless, we will frequently define trivial functions for exactly this sort of
reason.
Having built summation we can now easily construct more complicated functions. For
example, if we want to approximate π, we can do so using the Liebnitz formula, shown
below.
X∞
(−1)i+1 1 1 1 π
= 1 − + − + ... =
i=1
2i − 1 3 5 7 4
Given this equation, we can write a function pi_approx that computes the value of π implied
by summing the series up to a given point.
def pi_approx ( n ):
def pi_term ( i ):
if i % 2 == 1:
return 1/(2* i - 1)
else :
return -1/(2* i - 1)
return 4 * summation (n , pi_term )
This code functions exactly like the previous code for sum_squares — it just has a more
complicated function to compute each term. We’ve also moved the definition of this function,
pi_term, inside the body of pi_approx. This is a good choice because pi_term is not a
function that is generally useful outside of this particular application, and defining it outside
of pi_approx would clutter the namespace. Putting the definition outside of pi_approx also
creates the risk that pi_term is assigned another value, breaking pi_approx.
Unfortunately, having functions defined inside other functions adds new complexity to
our rules for how variable assignment and evaluation works. We’ll deal with these in the
next section.
The variable a is set equal to the output of example(). That in turn is equal to the returned
value of func(). So what does func do? It returns z. But z isn’t assigned as an input
value of func, nor is it assigned in the body of func, so there won’t be a z in the local
frame of func. According to the rule we gave before, this means that Python looks in the
global frame for z. and that would mean Python finds 3, and that is the value of z. This is
CHAPTER 2. PUSHING THE LIMITS 37
actually wrong, though — the value of z will be 2. The reason is that we have once again
left out crucial details of the rule.
In reality, every local frame in Python has a designated parent frame. When a variable
is not found in a given frame, Python checks its parents frame. If the variable is still not
found, Python will check that parent frame’s parent frame, and so on. That chain is called
an environment, and it will always lead eventually to the global frame. If the variable is
not found anywhere in the environment Python will report an error. The reason we did
not get into this complexity in subsection 2.1.1 is that so far every frame we saw had the
global frame as its parent frame. Here though, we see that the parent frame of the frame
for func’s evaluation is not the global frame.
When a function is first defined, Python stores the frame in which the definition occurred.
That frame is the parent frame for all future local frames that are created when that function
is run. Let’s look at the environment diagram of the example code above.
z = 3
a = example ()
Figure 2.5: An environment diagram with a function that was defined inside a local frame.
When the function func is defined, we now make a note next to it that the frame where
it was defined was f1. (Crucially, this note is attached to the function value itself, not to the
func variable.) Whenever this function is called the local frame that is created will have f1
as its parent frame. As a result, when func() is evaluated, the value found for z will be 2.
The formal rules (with all the necessary complexity) for assigning and evaluating variable
values are below:
Variable assignment When a variable is assigned, a row is created in the current frame
listing the variable and its value. If a row already exists with that variable name, it is simply
updated with the new value.
Variable evaluation When a variable is evaluated, the current frame is checked for a
corresponding value. If none is there, move to the parent frame and repeat. Keep moving
up until the variable name is found. If the global frame is reached without finding the
variable name, report an error.
CHAPTER 2. PUSHING THE LIMITS 38
Evaluating a function call When a call expression is encountered, first evaluate the
operator and operands in the current frame. The operator should evaluate to a function.
Create a new local frame for this function call. Set the parent of this frame to the frame
where the function was first defined. Then assign each of the function’s input variables to
the corresponding operand value. Finally, execute the code in the body of the function
definition.
In order to write code that works when these rules are followed, you need to understand
these rules and be able to carry them out by hand. There are two important things to keep
in mind when doing this. First, the parent frame of a function’s frame is the frame where
the function was defined, not the frame where it is being called. Second, the “function” that
we’re referring to is the actual function value, not the variable name that happens to point
to it currently. Consider the following example.
z = 3
f = example ()
a = f ()
In this case a function is defined under the name func in frame f1, the frame for the
call to example. That function is the returned value of example, so f is set equal to that
function in the global frame. When f is run, the associated parent frame is f1, not the global
frame. It might be tempting to think that because the f() expression is being evaluated in
the global frame that the global frame should be the parent, but where the function is being
called is irrelevant. Similarly, it might be tempting to think that because f was defined in
the global frame the global frame should be the parent, but where f was defined is irrelevant.
The function that f is pointing to was created in frame f1, so that is the parent frame.
Creating a function that outputs another function like this probably seems weird if you
haven’t seen it before. Certainly example is not a particularly useful function. Next, we’ll
show a more realistic setting where this might be used.
some constant to their input. For example, say we need a function add_one that takes as
input a single number n and outputs n+1. Similarly, add_two is a function that takes n
and outputs n+2. These are very simple functions, and we could just write each one from
scratch, but maybe we need a lot. Or maybe which ones we need will be determined during
the execution of our program, so we can’t make them ahead of time. What we can do is
make a function that constructs these adder functions on demand. Below is code defining
and demonstrating such a function.
def make_adder ( x ):
def adder ( y ):
return x + y
return adder
a = add_one (3)
b = add_two (5)
Note that each time make_adder is called, it defines adder anew. And while each of these
adder functions is a different function, their bodies are identical. The reason they behave
differently is that they have different associated parent frames. When make_adder(1) is
evaluated, it creates a frame in which x is set to 1. When it then defines the adder function,
that adder function has an associated parent frame in which x is 1. Similarly, the adder
function returned by make_adder(2) will, when called, be evaluated in frames whose parent
frame has x bound to 2.
CHAPTER 2. PUSHING THE LIMITS 40
This pattern of coding is quite useful. In essence, the value of x that held when adder
was defined will remain constant anytime that instance of adder is called. (There’s not
even any danger of accidentally changing x, since once the constructing call to make-adder
is complete, other code cannot access its frame to change x.) Now, the function above
that makes adders is indeed something that could appear in a larger program, but more
commonly a function like this would be used when the functions being constructed are more
complex and differ only in very minimal ways.
Consider, for example, the functions we had before that summed the first n terms of a
given sequence. These “summers” are very similar. We can create a function that constructs
these summers automatically. The structure is exactly the same as that of make_adder. We
simply replace the simple addition with the more complex code of series summation.
def make_summer ( term ):
def summer ( n ):
i , total = 1 , 0
while i <= n :
total += term ( i )
i += 1
return total
return summer
def square ( x ):
return x * x
def pi_term ( i ):
if i % 2 == 1:
return 4/(2* i - 1)
else :
return -4/(2* i - 1)
This certainly works, but it’s obviously a little annoying to have to define identity when
that function isn’t useful as a stand-alone function. Its only value is as an input to
make_summer. The def statement here is in a sense unnecessary. You don’t need to be
able to refer to this function by the name identity going forward. It’s as if you could just
CHAPTER 2. PUSHING THE LIMITS 41
call abs(-3) but instead had to first set x=-3 and then call abs(x). It would be easier if,
just as one can refer directly to the value -3 without the need to assign it to a variable, one
could refer directly to the function that returns output equal to its input without the need
to first assign it to a variable.
Python lets you do this using something called a lambda expression. This would allow
us to replace the above code with a simpler single line:
sum_ints = make_summer ( lambda x : x )
A lambda statement directly represents a function value. The lambda keyword just tells
Python that this is a function. Then before the colon are input variables, separated by
commas. After the colon is a return value. You can think of lambda <inputs>: <ouput>
as meaning “the function that takes hinputsi and returns houtputi”. So lambda x: x*x
means “the function that takes as input x and outputs x*x”. You can still assign these
function values to variables if you want, just like a def statement does. The following to
definitions of square are completely equivalent:
square = lambda x : x * x
def square ( x ):
return x * x
In general def statements tend to be easier to read. Lambda statements also don’t allow
you to use many lines of complicated code or control structures like a while loop. Therefore,
for the vast majority of our functions we will continue to use def statements. But when you
need a simple function for input or output from another function, and that function won’t
need to be referred to by name in the future, lambda statements can be very useful.