Difficulty: Intermediate
Explain Python closures and the LEGB scoping rule. What does nonlocal do?
LEGB rule - Python looks up names in this order: 1. Local: current function scope 2. Enclosing: enclosing function scopes (for nested functions) 3. Global: module-level scope 4. Built-in: Python built-in names (len, print, etc.)
Closure: a nested function that captures free variables from its enclosing scope. The captured variables are stored in the closure's __closure__ attribute.
nonlocal: declares that a name refers to the enclosing scope's variable and allows assignment to it (without nonlocal, assignment creates a new local variable).
x = 'global'
def outer():
x = 'enclosing'
def inner():
# x = 'local' # Would shadow enclosing x
print(x) # Finds x in enclosing scope (LEGB)
inner()
outer() # enclosing
# Closure - counter factory
def make_counter(start=0):
count = start # Free variable captured by closure
def increment(step=1):
nonlocal count # Modify enclosing count
count += step
return count
def reset():
nonlocal count
count = start
def get():
return count
return increment, reset, get
inc, reset, get = make_counter(10)
print(inc()) # 11
print(inc(5)) # 16
print(get()) # 16
reset()
print(get()) # 10
# Inspect closure
print(inc.__code__.co_freevars) # ('count', 'start')
print(inc.__closure__[0].cell_contents) # 10
nonlocal allows modifying the enclosing scope's variable. Without it, count += step would create a new local count (UnboundLocalError).
# Classic gotcha: loop variable in closure
functions = []
for i in range(5):
functions.append(lambda: i) # All capture same i
result = [f() for f in functions]
print(result) # [4, 4, 4, 4, 4] - i is 4 at call time
# Fix 1: default argument captures current value
functions = []
for i in range(5):
functions.append(lambda i=i: i) # i=i captures current i
print([f() for f in functions]) # [0, 1, 2, 3, 4]
# Fix 2: factory function
def make_fn(i):
return lambda: i
functions = [make_fn(i) for i in range(5)]
print([f() for f in functions]) # [0, 1, 2, 3, 4]
# global keyword
total = 0
def add_to_total(n):
global total # Without this, assignment creates local total
total += n
add_to_total(5)
add_to_total(3)
print(total) # 8
The loop gotcha: closures capture the variable, not its value. The lambda captures the reference to i, which is 4 when all functions are called.
Closure, LEGB, nonlocal, global, Free Variables