RC: W4 D4 — How closures capture variables
March 7, 2024While I wanted to add tests on my locking mechanisms, I came upon an interesting bug. Here is a simplification of it:
lambda_functions = []
for i in range(5):
lambda_functions.append(lambda: i)
# Executing the lambda functions
results = [func() for func in lambda_functions]
print(results)
I would have expected this to print [0, 1, 2, 3, 4]
.
Instead, I got [4, 4, 4, 4, 4]
!
What is happening here comes from the way that closures capture variables:
- Python uses lexical scoping: this means that they remember the name of the closed-over variable (
i
in our example) where it is created. - But these variables are late-binding: this means that the value attached to the name is looked up when the closure is executed, not when it is created.
In this case, the lambda functions are executed outside the for
loop, when the i
variable is set to 4
. This is
why we get [4, 4, 4, 4, 4]
instead of [0, 1, 2, 3, 4]
.
If we set i = 7
and then re-run print([func() for func in lambda_functions])
then we would get: [7, 7, 7, 7, 7]
.
In order to resolve the problem, we can create a new scope for each lambda by defining an additional
function (create_lambda
) that captures the value of the loop variable (i
) by passing it as an argument (therefore,
each lambda has its own i
value):
def create_lambda(x):
return lambda: x
lambda_functions_corrected = []
for i in range(5):
lambda_functions_corrected.append(create_lambda(i))
# Executing the corrected lambda functions
corrected_results = [func() for func in lambda_functions_corrected]
print(corrected_results)
Here, the create_lambda
function is called at each iteration in the for
loop.
Thus, each lambda function that is returned has its own scope with a copy of i
at the time the lambda was created.
Therefore, the results printed are [0, 1, 2, 3, 4]
. The issue is solved! Hurray! 🎉
For information, there is another way to handle this: we can capture the value of i
at each iteration by using it as a
default parameter value for the lambda function.
Here is what it looks like:
lambda_functions_corrected = []
for i in range(5):
lambda_functions_corrected.append(lambda i=i: i)
# Executing the corrected lambda functions
corrected_results = [func() for func in lambda_functions_corrected]
print(corrected_results)
For reference, here is where I found the explanation for it!