Closures

By Martin McBride, 2019-09-28
Tags: closure inner function function composition
Categories: functional programming


Closures are a powerful and flexible way to create new functions out of existing functions. You can think of them as being function factories, that can create new functions according to a template and one or more parameters.

The Python implementation relies on inner functions.

Inner functions

An inner function is a function that is defined within the scope of another function, like this:

def add_3(v):
    n = 3
    def f(x):
        return x + n
    return f(v)

print(add_3(6))   # Prints 9

In this case our outer function add_3 adds 3 to the value of x and returns the result.

The add_3 function contains:

  • A local variable n that is set to 3.
  • An inner (or 'local') function f that adds n to the supplied parameterx

It uses these to calculate the result of v + 3. Of course, you could write a much simpler function to do the same job, we are simply using it to illustrate the idea of inner functions.

The local variable n cannot be accessed from outside the print_add_3 function. obviously, since it is a local variable. In a similar way, the inner function f can't be accessed from outside the print_add_3 function either, it is also local. This makes sense, of course, the reason we use local variables and inner functions is to hide the inner workings of our function from the outside world. This is called encapsulation.

It is worth noting that the function f is able to access the variable n, because they are both within the scope of the outer function print_add_3.

Functions as objects

As an aside, though quite an important one, you may have heard it said that in Python everything is an object. Well that isn't just true of numbers, strings, lists etc, it is true of functions too! So:

print(min(1, 2))

calls the function min, and prints the result. But without the brackets:

print(min)

will display <built-in function min>, the function object for min. You can even assign this function to a variable, and then call it using bracket notation:

a = min
print(a(3, 2))

This will print 2, because a references the min function object, and a(3, 2) calls it. We call this aliasing: a is an alias of min.

Returning an inner function

Now we will rearrange things a little:

def get_add_3():
    n = 3
    def f(x):
        return x + n
    return f

add_3 = get_add_3()
print(add_3(6))    # Prints 9

The first thing to notice here is that we get_add_3 returns f. It doesn't call f, it returns the function object f.

So our new function get_add_3 doesn't add 3 to a number, instead it returns a function object f that adds 3 to a number. This means that we need two steps to add 3 to a number:

  • We call get_add_3, which returns a function object, which we store as add_3.
  • We then call add_3(6), which uses the function object to do the calculation.

Now you might have noticed something slightly odd here. add_3 is an alias of f, and f uses the value of n, which is a local variable of get_add_3. But add_3 is being called outside the context of get_add_3, because we have already returned from the call.

Now add_3 obviously knows the value of n, because it knows to add 3 to its arguments. So what is going on?

Well, when the function f is returned, Python attaches extra information to the function object, storing the values of any free variables within the outer function.

In fact, this is a closure, it is just that at the moment it isn't a particularly useful closure.

Closures as function factories

The final step is to make this into something more useful:

def add_n(n):
    def f(x):
        return x + n
    return f

add_4 = add_n(4)
add_7 = add_n(7)
print(add_4(6))   # Prints 10
print(add_7(5))   # Prints 12

This time, when we call add_n, we actually pass in our required n value. The function returns a closure of f that includes the requested value of n.

So add_n(4) creates a function that adds 4 to a value. We can store this in a variable add_4 and then call add_4() as a function - because that is exactly what it is. Likewise, we can create an add_7 function if we need one, or any n we require.

Closures are basically function factories, and very useful ones at that.

Why bother?

You might wonder why we would go to all the trouble of creating a closure to add 4 to x. Why not just do:

y = x + 4

Well, in functional programming you generally want to deal with functions rather than expressions. Of course we could do this:

def add_4(x)
    return x + 4

But that requires us to hand write a function, which could easily introduce bugs. Now assuming we have an add_n closure already defined, and we trust it because it is part of a well tested library, we can just do this:

add_4 = add_n(4)

This is far more declarative - we are saying what we want rather than describing how to do it. It is both more readable and less error prone.

Definition of a closure

To summarise what we have done so far, a closure requires three things:

  • An outer function that contains an inner function.
  • The outer function has parameters and/or local variables.
  • The outer function returns the inner function as a function object.

Example - composing functions

Suppose we wanted to take an object, convert it to a string, and find how long that string is. We could do it like this:

x = len(str(3*10))  # x = 2 because str(3*10) is '30' and len('30') is 2

That is all very well, but what if we wanted to apply this operation to a list of values using the map function. map requires a function object and a list or other iterable. So we would have to create a special lenstr functions:

def lenstr(x):
    return len(str(x))

map(lenstr, values)

This would work but is it a very procedural approach. Again, we are defining a special function, which is error prone.

Suppose instead we had a compose closure, that takes two functions, f and g and returns a new function that finds f(g(x)):

def compose(f, g):
    def comp(x):
        return f(g(x))
    return comp

compose is quite a general purpose function, available in several popular Python functional programming libraries. Now we can do this:

lenstr = compose(len, str)

This creates a lenstr function without any procedural coding. Or we can just use compose directly without storing the function:

map(compose(len, str), values)

Again, this is far more declarative. Rather than hand coding a special lenstr function, we are simply declaring a function that is a composition of len and str. It is clearer and more reliable.

See also

If you found this article useful, you might be interested in the book NumPy Recipes or other books by the same author.

Join the PythonInformer Newsletter

Sign up using this form to receive an email when new content is added:

Popular tags

2d arrays abstract data type alignment and angle animation arc array arrays bar chart bar style behavioural pattern bezier curve built-in function callable object chain circle classes clipping close closure cmyk colour combinations comparison operator comprehension context context manager conversion count creational pattern data science data types decorator design pattern device space dictionary drawing duck typing efficiency ellipse else encryption enumerate fill filter font font style for loop formula function function composition function plot functools game development generativepy tutorial generator geometry gif global variable gradient greyscale higher order function hsl html image image processing imagesurface immutable object in operator index inner function input installing iter iterable iterator itertools join l system lambda function latex len lerp line line plot line style linear gradient linspace list list comprehension logical operator lru_cache magic method mandelbrot mandelbrot set map marker style matplotlib monad mutability named parameter numeric python numpy object open operator optimisation optional parameter or pandas partial application path pattern permutations pie chart pil pillow polygon pong positional parameter print product programming paradigms programming techniques pure function python standard library radial gradient range recipes rectangle recursion reduce regular polygon repeat rgb rotation roundrect scaling scatter plot scipy sector segment sequence setup shape singleton slice slicing sound spirograph sprite square str stream string stroke structural pattern subpath symmetric encryption template tex text text metrics tinkerbell fractal transform translation transparency triangle truthy value tuple turtle unpacking user space vectorisation webserver website while loop zip zip_longest