What is a closure, and what's the late-binding pitfall with closures in loops?
Quick Answer
A closure is a nested function that remembers variables from its enclosing scope even after that scope has finished executing, by keeping a reference to the enclosing variable (a "cell"), not a snapshot of its value at creation time. The classic pitfall: closures created in a loop (e.g., a list of lambdas) all share the same loop variable. By the time they're called, they all see its final value, not the value at the time each closure was created.
Detailed Answer
What makes something a closure
def make_multiplier(factor):
def multiply(x):
return x * factor # `factor` is captured from the enclosing scope
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
double(5) # 10
triple(5) # 15
multiply is a closure over factor. Even after make_multiplier
returns, multiply still has access to its own factor, because Python
keeps the enclosing variable alive as long as any closure references it.
double and triple each capture a different factor because each
call to make_multiplier creates a fresh scope.
The late-binding trap
funcs = []
for i in range(3):
funcs.append(lambda: i)
[f() for f in funcs] # [2, 2, 2] -- NOT [0, 1, 2]!
Every lambda captures the variable i by reference, not its value at
the time the lambda was created. All three lambdas share the same
enclosing scope, since the loop body doesn't create a new scope per
iteration (loops don't introduce scopes in Python at all). By the time
they're called, i has already finished the loop and holds its final
value, 2.
The fix: bind the value via a default argument
funcs = []
for i in range(3):
funcs.append(lambda i=i: i) # default arg is evaluated NOW, at def-time
[f() for f in funcs] # [0, 1, 2]
Default argument values are evaluated once, when the lambda/def is
created. Using i=i copies the current value of i into the
function's own default, decoupling it from the loop variable's later
mutations.
Alternative fix: a factory function
def make_getter(value):
return lambda: value # closes over `value`, a fresh parameter per call
funcs = [make_getter(i) for i in range(3)]
[f() for f in funcs] # [0, 1, 2]
Each call to make_getter creates a genuinely new scope with its own
value, so each returned lambda captures a distinct variable. This is
the same principle as make_multiplier above, just applied to fix the
loop pitfall.
Closures capture variables by reference to the enclosing scope, not by value. This is powerful for building factories/decorators, but it means closures created in a loop all share the loop variable and see its final value, unless you force early binding (the default argument trick or a separate factory function per iteration).