Closures in Python

Closures in Python

A closure refers to a situation where one function is defined inside another function, and the inner function references a variable from the outer function (not a global variable). Furthermore, the outer function returns the inner function. Thus, the inner function, together with the external variables it references, forms a closure.

1. Basic Concept of Closures

  • The formation of a closure requires three conditions:
    • There must be a nested function (a function defined inside another function).
    • The nested function must reference a variable defined in the outer function (i.e., a free variable).
    • The outer function must return the nested function (rather than calling it).

2. Simple Example of a Closure
Suppose we need a function that can record and return the number of times it has been called:

def counter():
    count = 0  # Variable of the outer function
    def inner():
        nonlocal count  # Declare count as nonlocal to allow modification
        count += 1
        return count
    return inner  # Return the inner function, not the result of calling it

# Create an instance of the closure
c = counter()
print(c())  # Output: 1
print(c())  # Output: 2
  • Here, counter is the outer function and inner is the nested function.
  • inner references the external variable count, and counter returns the inner function.
  • When calling c = counter(), c actually points to the inner function but retains the state of the count variable, even after the counter function has finished executing.

3. How Closures Work

  • In Python, functions are first-class objects and can be passed around and returned like variables.
  • When the outer function executes, it creates a local scope containing its local variables (e.g., count).
  • The inner function remembers the scope in which it was defined (i.e., the local scope of the outer function). Even after the outer function ends, the inner function can still access these variables.
  • This "memory" is implemented through the __closure__ attribute of the function, which stores all referenced external variables.

4. Inspecting Closure Details
The __closure__ attribute can be used to observe the variables referenced by the closure:

def example(x):
    def inner(y):
        return x + y
    return inner

f = example(10)
print(f.__closure__)        # Output: (<cell at 0x...: int object at 0x...>,)
print(f.__closure__[0].cell_contents)  # Output: 10 (the value of the external variable x)
  • __closure__ is a tuple containing "cells" for all referenced external variables.
  • The cell_contents attribute of each cell stores the actual value of the variable.

5. Closures and Mutable State

  • If a closure needs to modify an external variable, it must use the nonlocal declaration (Python 3 and above):
def accumulator(start=0):
    total = start
    def add(n):
        nonlocal total  # Declare total as nonlocal
        total += n
        return total
    return add

acc = accumulator(10)
print(acc(5))  # Output: 15
print(acc(3))  # Output: 18
  • Without nonlocal, total += n would be treated as creating a new local variable inside add, leading to an error.

6. Common Use Cases for Closures

  • State Preservation: Such as counters, accumulators, avoiding the use of global variables.
  • Deferred Calculation: Closures can capture some parameters first and perform calculations later upon subsequent calls.
  • Decorators: Decorators themselves are an application of closures; they take a function as an argument and return a new function.

7. Precautions

  • Closures may cause memory leaks because they hold references to external variables for a long time.
  • Be cautious about variable binding when creating closures in loops:
# Incorrect example: all closures share the same i
functions = []
for i in range(3):
    def inner():
        return i
    functions.append(inner)
print([f() for f in functions])  # Output: [2, 2, 2] (not the expected [0,1,2])

# Correct approach: use default parameters to capture the current value
functions = []
for i in range(3):
    def inner(x=i):  # Bind the current value of i using a default parameter
        return x
    functions.append(inner)
print([f() for f in functions])  # Output: [0, 1, 2]

Through the steps above, the core mechanisms and application scenarios of closures are clearly presented. By nesting functions and referencing variables, closures enable state encapsulation and persistence.