The Lazy Closure
You are writing a function that creates a list of multiplier functions:
def make_multipliers(n):
return [lambda x: x * i for i in range(n)]
multipliers = make_multipliers(3)
for m in multipliers:
print(m(10))
The developer expects the output to be 0, 10, 20. Instead, it prints 20, 20, 20. The developer then tries:
def make_multipliers(n):
return [lambda x, i=i: x * i for i in range(n)]
Now it prints 0, 10, 20 as expected. The developer is confused about why the first version fails and why the second works.
1. Explain why the first version prints `20` three times. What is the role of late binding in this behavior?
Read the worked answer
In the first version, the lambda functions capture the variable i by reference, not by value. All lambdas share the same i from the enclosing scope of make_multipliers. The list comprehension iterates i from 0 to 2, but the lambdas are not called until after the comprehension completes. By that time, i has the value 2. So when each lambda is called with x=10, it computes 10 * 2 = 20. This is known as late binding: the value of i is looked up at call time, not at definition time. The second version uses a default argument i=i, which captures the current value of i at the time each lambda is defined. Default arguments are evaluated at definition time, so each lambda gets its own copy of i (0, 1, 2). Thus the outputs are 0, 10, 20.
2. If you wanted to avoid the default argument trick and instead use a factory function, how would you rewrite `make_multipliers` to achieve the same correct output? Provide code and explain why it works.
Read the worked answer
You can use a factory function that returns a lambda, like this:
def make_multipliers(n):
def multiplier(i):
return lambda x: x * i
return [multiplier(i) for i in range(n)]
Here, each call to multiplier(i) creates a new scope with its own i parameter. The lambda closes over that specific i from the multiplier function's local scope. Since each multiplier call has a distinct i, the lambdas capture the correct values. This works because each invocation of multiplier creates a new closure with its own binding for i, avoiding the shared variable issue.