Closures and Composition#
A higher-order function can return a function as well as take one. When it does, the returned function often needs to remember something from the moment it was created. A function bundled together with the variables it remembers from its enclosing scope is called a closure.
Functions That Build Functions#
make_multiplier below takes a number and returns a new function that
multiplies by it. The inner multiply uses factor, which belongs to
the enclosing call — and it keeps working even after make_multiplier
has returned, because the closure holds on to that value:
>>> def make_multiplier(factor):
... def multiply(x):
... return x * factor
... return multiply
...
>>> triple = make_multiplier(3)
>>> double = make_multiplier(2)
>>> triple(10)
30
>>> double(10)
20
triple and double are two different closures built from the same
code, each remembering its own factor. You have already used a closure
without naming it: the helper inside binary_search in the
Recursion chapter closes over arr and
key so it does not have to pass them through every recursive call.
A closure can also remember something that changes. make_counter
returns a function that hands back 1, 2, 3, … on successive calls; the
nonlocal keyword lets the inner function update the count in the
enclosing scope:
>>> def make_counter():
... count = 0
... def next_value():
... nonlocal count
... count += 1
... return count
... return next_value
...
>>> c = make_counter()
>>> c()
1
>>> c()
2
>>> c()
3
Notice the trade-off: this counter is convenient, but it is no longer a
pure function. Calling c() twice with no arguments gives two different
answers, because it carries hidden state. Closures let you choose — keep
them pure when you can, and reach for mutable state only when the problem
genuinely needs memory.
Fixing Some Arguments: partial#
A common reason to build a function from another is to fix some of its
arguments. functools.partial does exactly that: give it a function and
some arguments, and it returns a new function that supplies them for you.
>>> from functools import partial
>>> def power(base, exponent):
... return base ** exponent
...
>>> square = partial(power, exponent=2)
>>> cube = partial(power, exponent=3)
>>> square(9)
81
>>> cube(2)
8
square is power with exponent nailed down to 2. This is a
tidy alternative to writing lambda x: power(x, 2) and works well as a
key or as an argument to map.
Composing Functions#
If map, filter, and reduce are about applying functions to data,
composition is about combining functions with each other. Mathematics
writes the composition of f and g as f ∘ g, meaning “apply g, then
apply f.” We can build a compose helper that does this for any number
of functions, right to left, using reduce:
def compose(*funcs):
"""Return a function that applies funcs right to left.
compose(f, g)(x) == f(g(x)). With no arguments it returns the
identity function, so composition has a sensible starting point.
"""
return reduce(lambda f, g: lambda x: f(g(x)), funcs, lambda x: x)
>>> from functools import reduce
>>> def compose(*funcs):
... return reduce(lambda f, g: lambda x: f(g(x)), funcs, lambda x: x)
...
>>> double_then_increment = compose(lambda x: x + 1, lambda x: x * 2)
>>> double_then_increment(10)
21
The result of double_then_increment(10) is 21: the rightmost
function runs first (10 * 2 is 20), then the next (20 + 1 is
21). Composition lets you assemble a complex transformation out of
small, individually testable steps — which is exactly the property the next
two sections depend on.