Python Closures#

Learn about Python closure, how to define a closure, and the reasons you should use it.

Before understand Python Closure let us try to understand first Higher Order Functions

Higher Order Functions#

In Python functions are treated as first class citizens, allowing you to perform the following operations on functions:

  • A function can take one or more functions as parameters

  • A function can be returned as a result of another function

  • A function can be modified

  • A function can be assigned to a variable

In this sub-section, we will cover:

  1. Handling functions as parameters

  2. Returning functions as return value from another functions

Function as a Parameter#

def sum_numbers(nums):  # normal function
    return sum(nums)    # a sad function abusing the built-in sum function :<

def higher_order_function(f, lst):  # function as a parameter
    summation = f(lst)
    return summation
result = higher_order_function(sum_numbers, [1, 2, 3, 4, 5])
print(result)       # 15
15

Function as a Return Value#

def square(x):          # a square function
    return x ** 2

def cube(x):            # a cube function
    return x ** 3

def absolute(x):        # an absolute value function
    if x >= 0:
        return x
    else:
        return -(x)

def higher_order_function(type): # a higher order function returning a function
    if type == 'square':
        return square
    elif type == 'cube':
        return cube
    elif type == 'absolute':
        return absolute

result = higher_order_function('square')
print(result(3))       # 9
result = higher_order_function('cube')
print(result(3))       # 27
result = higher_order_function('absolute')
print(result(-3))      # 3
9
27
3

You can see from the above example that the higher order function is returning different functions depending on the passed parameter

Nonlocal variable in a nested function#

Before getting into what a closure is, we have to first understand what a nested function and nonlocal variable is.

A function defined inside another function is called a nested function. Nested functions can access variables of the enclosing scope.

In Python, these non-local variables are read-only by default and we must declare them explicitly as non-local (using nonlocal keyword) in order to modify them.

Following is an example of a nested function accessing a non-local variable.

def print_msg(msg):
    # This is the outer enclosing function

    def printer():
        # This is the nested function
        print(msg)

    printer()

# We execute the function
# Output: Hello
print_msg("Hello")
Hello

We can see that the nested printer() function was able to access the non-local msg variable of the enclosing function.

Defining a Closure Function#

Python allows a nested function to access the outer scope of the enclosing function. This is is known as a Closure. Let us have a look at how closures work in Python. In Python, closure is created by nesting a function inside another encapsulating function and then returning the inner function.

In the example above, what would happen if the last line of the function print_msg() returned the printer() function instead of calling it? This means the function was defined as follows:

def print_msg(msg):
    # This is the outer enclosing function

    def printer():
        # This is the nested function
        print(msg)

    return printer  # returns the nested function


# Now let's try calling this function.
# Output: Hello
another = print_msg("Hello")
another()
Hello

That’s unusual.

The print_msg() function was called with the string "Hello" and the returned function was bound to the name another. On calling another(), the message was still remembered although we had already finished executing the print_msg() function.

This technique by which some data (”"Hello" in this case) gets attached to the code is called closure in Python.

This value in the enclosing scope is remembered even when the variable goes out of scope or the function itself is removed from the current namespace.

Try running the following in the Python shell to see the output.

del print_msg
another()
Hello
print_msg("Hello")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-6-af0f59439c9b> in <module>
----> 1 print_msg("Hello")

NameError: name 'print_msg' is not defined

Here, the returned function still works even when the original function was deleted.

When do we have closures?#

As seen from the above example, we have a closure in Python when a nested function references a value in its enclosing scope.

The criteria that must be met to create closure in Python are summarized in the following points.

  1. We must have a nested function (function inside a function).

  2. The nested function must refer to a value defined in the enclosing function.

  3. The enclosing function must return the nested function.

When to use closures?#

So what are closures good for?

Closures can avoid the use of global values and provides some form of data hiding. It can also provide an object oriented solution to the problem.

When there are few methods (one method in most cases) to be implemented in a class, closures can provide an alternate and more elegant solution. But when the number of attributes and methods get larger, it’s better to implement a class.

Here is a simple example where a closure might be more preferable than defining a class and making objects. But the preference is all yours.

# Example:

def make_multiplier_of(n):
    def multiplier(x):
        return x * n
    return multiplier


# Multiplier of 3
times3 = make_multiplier_of(3)

# Multiplier of 5
times5 = make_multiplier_of(5)


print(times3(9))  # Output: 27

print(times5(3))  # Output: 15

print(times5(times3(2)))  # Output: 30
27
15
30
# Example:

def add_ten():
    ten = 10
    def add(num):
        return num + ten
    return add

closure_result = add_ten()
print(closure_result(5))  # 15
print(closure_result(10))  # 20
15
20

Python Decorators make an extensive use of closures as well.

On a concluding note, it is good to point out that the values that get enclosed in the closure function can be found out.

All function objects have a __closure__ attribute that returns a tuple of cell objects if it is a closure function. Referring to the example above, we know times3 and times5 are closure functions.

make_multiplier_of.__closure__
times3.__closure__
(<cell at 0x0000012C87C0AF70: int object at 0x00007FFAD7CB2770>,)

The cell object has the attribute cell_contents which stores the closed value.

times3.__closure__[0].cell_contents
3
times5.__closure__[0].cell_contents
5