Closures and Variable Scope in Python
Description:
Closures are an important concept in functional programming, referring to a situation where one function (called the outer function) defines another function (called the inner function) inside it, and the inner function references a local variable from the outer function (called a free variable). Even after the outer function has finished executing, these referenced variables are not destroyed. Instead, they form a closed entity together with the inner function, which is the closure. Understanding closures requires a deep grasp of Python's variable scope (LEGB rule) and the lifecycle of variables.
Problem-solving Process/Knowledge Explanation:
Step 1: Understanding Variable Scope (LEGB Rule)
In Python, when accessing a variable, the interpreter searches in the following order:
- L (Local): Local scope, inside the current function.
- E (Enclosing): The scope of the outer function (for closure functions).
- G (Global): Global scope, at the module level.
- B (Built-in): Built-in scope, Python's built-in identifiers (e.g.,
len,range).
Example:
x = "global" # Global scope
def outer():
y = "enclosing" # Enclosing scope
def inner():
z = "local" # Local scope
print(z) # Finds local variable z
print(y) # Finds closure variable y (not in current function → look outward)
print(x) # Finds global variable x (not in enclosing scope → look globally)
print(len) # Finds built-in function len
inner()
outer()
Step 2: Recognizing the Basic Structure of a Closure
A closure must satisfy three conditions:
- Function nesting (outer function contains inner function).
- The inner function references a variable from the outer function.
- The outer function returns the inner function (note: the function object itself, not the result of its call).
def outer_func(x): # Outer function, x is a local variable
def inner_func(y): # Inner function
return x + y # References outer function variable x
return inner_func # Returns inner function object
closure = outer_func(10) # outer_func finishes execution, but variable x (value 10) is preserved
result = closure(5) # 10 + 5 = 15
print(result) # Outputs 15
Step 3: Understanding the Lifecycle of Closure Variables
Ordinary local variables are destroyed after the function finishes execution, but free variables in a closure have an extended lifecycle:
def counter():
count = 0 # An ordinary local variable, but becomes a closure variable when referenced by inner function
def increment():
nonlocal count # Declares that count comes from the outer scope
count += 1
return count
return increment
# Create two independent counters
counter1 = counter()
counter2 = counter()
print(counter1()) # 1 (counter1's count goes from 0→1)
print(counter1()) # 2 (counter1's count goes from 1→2)
print(counter2()) # 1 (counter2 has its own independent count variable, goes from 0→1)
Step 4: Deep Dive into How Closure Variables Are Stored
Closure variables are actually stored in the __closure__ attribute of the inner function:
def make_closure(x):
def closure_func():
return x
return closure_func
closure = make_closure(100)
print(closure()) # 100
# Inspect closure variables
print(closure.__closure__) # A tuple containing cell objects
print(closure.__closure__[0].cell_contents) # 100
Step 5: Classic Application Scenarios of Closures
- Function Factories: Creating functions with similar functionality but different configurations.
def power_factory(exponent):
def power(base):
return base ** exponent
return power
square = power_factory(2) # Creates a squaring function
cube = power_factory(3) # Creates a cubing function
print(square(5)) # 25
print(cube(5)) # 125
- Decorators: Implemented based on closures (the foundation of decorators mentioned earlier).
def logger(func):
def wrapper(*args, **kwargs):
print(f"Calling function: {func.__name__}")
return func(*args, **kwargs)
return wrapper
- State Maintenance: Replaces global variables, providing better encapsulation.
def bank_account(initial_balance):
balance = initial_balance
def deposit(amount):
nonlocal balance
balance += amount
return balance
def withdraw(amount):
nonlocal balance
if amount <= balance:
balance -= amount
return balance
else:
return "Insufficient balance"
return deposit, withdraw
deposit, withdraw = bank_account(100)
print(deposit(50)) # 150
print(withdraw(30)) # 120
Step 6: Common Pitfalls and Precautions
- Late Binding Issues: Be cautious when creating closures inside loops.
# Incorrect example: All closures reference the same i
functions = []
for i in range(3):
def func():
return i
functions.append(func)
print([f() for f in functions]) # [2, 2, 2] Not the expected [0, 1, 2]
# Correct approach: Use default arguments or create a new scope
functions = []
for i in range(3):
def func(x=i): # Default arguments are evaluated at definition time
return x
functions.append(func)
print([f() for f in functions]) # [0, 1, 2]
- Modifying Closure Variables: Requires the
nonlocalkeyword.
def counter():
count = 0
def increment():
nonlocal count # Must be declared to modify
count += 1
return count
return increment
Closures are the fundamental mechanism in Python for implementing advanced features like decorators, callback functions, and function factories. Understanding closures helps in writing more elegant and flexible code.