This article originally appeared on Walker AI

What is a closure in Python? What are closures good for? Why use closures? Today we take these three questions step by step to understand closures.

Closures and functions are so closely related that it is necessary to give some background on concepts such as nested functions and the scope of variables.

1. Scope

The scope is the scope within which variables can be accessed at runtime. Variables defined within a function are local variables. The scope of a local variable can only be within the scope of the function, and it cannot be referenced outside the function.

A variable defined in the outermost layer of a module is a global variable. It is visible globally and can be read from within a function. Local variables cannot be accessed outside a function. Such as:

a = 1 
def foo() : 
   print(a) # 1 
Copy the code
def foo() : 
    print(a) # NameError: name 'num' is not defined 
Copy the code

2. Nested functions

A function can be defined not only in the outermost layer of a module, but also inside another function. A function defined within a function is called a nested function. For a nested function, it can access non-local variables declared in its outer scope. For example, the variable A in the code example can be accessed normally by a nested function printer.

def foo() : 
   #foo is a peripheral function
   a = 1 
   # printer is a nested function
   def printer() : 
       print(a)
   printer() 
foo() # 1
Copy the code

Is there any way that local variables can be accessed even outside the scope of the function itself?

The answer is closures!

Let’s change the above function to higher-order (take a function as an argument, or return a function as a result).

def foo() : 
   #foo is a peripheral function
   a = 1 
   # printer is a nested function
   def printer() : 
       print(a)
   return printer
x = foo() 
x() # 1
Copy the code

This code has exactly the same effect as the previous example, again printing 1. The difference is that the internal function printer is returned directly as a return value.

In general, local variables in a function are only available for the duration of the function’s execution. Once foo() has been executed, we assume that variable a is no longer available. However, here we find that after foo is executed, the value of a is printed normally when x is called. This is what closures do. Closures make it possible for local variables to be accessed outside of the function.

3. The closure

People sometimes confuse closures with anonymous functions. There’s a historical reason for this: defining functions inside functions wasn’t common until you started using anonymous functions. Also, closures are a problem only when nested functions are involved. So, a lot of people know both at the same time.

In fact, a closure is a function that extends its scope and contains non-global variables that are referenced in the body of the function definition, but not defined in the body. It doesn’t matter if the function is anonymous; the key is that it can access non-global variables defined outside the body.

In general terms, a closure, as the name implies, is a closed wrapper that encloses a free variable, just like the value of an attribute defined in a class. The scope of the free variable is visible along with the wrapper, and the free variable can be accessed wherever the wrapper is. So where is this package bound? Add a print to the above code:

  def foo() :
       # foo is a peripheral function
       a = 1
       # printer is a nested function
       def printer() :
           print(a)
       return printer
x = foo()
print(x.__closure__[0].cell_contents) # 1 
Copy the code

As you can see in the __closure__ attribute of the function object, __closure__ is a meta-object function responsible for the closure binding, that is, the binding of free variables. The value is usually None, and if the function is a closure, it returns a tuple of cell objects. The cell_contents property of the cell object is a free variable in the closure. This explains why a local variable can be accessed outside of the function even after it leaves the function, because it is stored in the closure’s cell_contents.

4. Benefits of closures

Closures avoid the use of global variables, and in addition, closures allow functions to be associated with some of the data (environment) on which they operate. This is very similar to object-oriented programming, where objects allow us to associate certain data (properties of an object) with one or more methods.

In general, when there is only one method in an object, using closures is a better choice. Let’s take an example of calculating the mean. Let’s say I have a function called AVg, and what it does is it calculates the mean of an increasing series of values; For example, the average closing price of a commodity over history. New prices are added each day, so the average takes into account all the prices so far, as shown below:

>>> avg(10) # 10.0
>>> avg(11) # 10.5
>>> avg(12) # 11.0
Copy the code

In the past, we could design a class:

class Averager(): def __init__(self): self.series = [] def __call__(self, new_value): Append (new_value) total = sum(self.series) return total/len(self.series) avg = Averager() avg(10) #10.0 Avg (11) # 10.5 avg (12) # 11.0Copy the code

We use closures to do this.

def make_averager(): series = [] def averager(new_value): series.append(new_value) total = sum(series) return total/len(series) return averager avg = make_averager() avg(10) Avg (11) # 10.5avg (12) #11.0Copy the code

When make_averager is called, an averager function object is returned. Each time averager is called, it adds arguments to the list and calculates the current average. This is more elegant than using classes, and decorators are also based on closures.

5. Closure pits

After looking at the above explanation of closures, you think that’s all closures are? In practice, there are often inadvertent traps to fall into. Take a look at the following example:

def create_multipliers() :
   return [lambda x: x * i for i in range(5)]
    
for multiplier in create_multipliers():
   print(multiplier(2))
    
# Expect output 0, 2, 4, 6, 8
# The result is 8, 8, 8, 8, 8
Copy the code

We expect output 0, 2, 4, 6, 8. Eight, eight, eight, eight, eight, eight. Why is this a problem? Let’s change the code:

def create_multipliers() :
   multipliers = [lambda x: x * i for i in range(5)]
   print([m.__closure__[0].cell_contents for m in multipliers])
        
create_multipliers()  # [4, 4, 4, 4]
Copy the code

You can see that the value of I in the function binding is 4, which is the final value of I after the loop. This is because Python closures are delayed binding, which means that the value of the variable used in the closure is queried when the inner function is called.

The correct way to use it is to pass the value of I as a parameter:

def create_multipliers() :
   return [lambda x,i=i: x * i for i in range(5)]
 
s = create_multipliers()
for multiplier in s:
   print(multiplier(2))  # 0, 2, 4, 6, 8

Copy the code

We pass I with the default argument, which is bound to the __defaults__ property just like the closure.

print([f.__defaults__ for f in s]) # [(0,), (1,), (2,), (3,), (4,)] 
Copy the code

PS: more dry technology, pay attention to the public, | xingzhe_ai 】, and walker to discuss together!