Here is a very entertaining Decode The Code puzzle to analyze.
funs = []
for i in range(4):
funs.append(lambda: i)
for i in funs:
print(i())
What could be the result of such an innocent code? Or should I call it diabolical? GPT-o1, the most advanced model from OpenAI to date, has not been able to provide me with a valid explanation, even after showing it the output of the code. Will we, mere humans, be able to do better than an LLM?
Output Of The Code
Expand the following box to see the output of running the code above.
And the output is…<function <lambda> at 0x108d0feb0> <function <lambda> at 0x108e3f2e0> <function <lambda> at 0x108e3f370> <function <lambda> at 0x108e3f400>
Four different lambda functions are being displayed on the screen. Everything seems to indicate that the second loop is actually printing the functions themselves and not the result of calling the lambda functions. How is that possible?
Moreover, if we call
funs[0]()
after executing the second loop, we will not get the first lambda function (0x108d0feb0
in my example), but rather the last one (0x108e3f400
).
Explaining the Behavior
In the Python documentation, there is a section called Why do lambdas defined in a loop with different values all return the same result? on which this problem is based; this problem is indeed an extension of it. If we remove the second loop and simply call the first function from the list, we find that instead of getting a 0 as we might expect, it returns a 3.
funs = []
for i in range(4):
funs.append(lambda: i)
assert funs[0]() == 0 # AssertionError! 3 != 0
As clearly explained in the previously provided link, i
is not local to the lambdas; it is declared in an outer scope. Furthermore, the value of i
returned by the different lambda functions is accessed when the function is called. Therefore, by the end of the loop, i = 3
, which is what it returns. All the four functions in funs
are returning 3.
By adding a second loop that uses i
again in the same scope, what is achieved is that each call to i()
actually returns itself.
Variations
Variation 1 - A Show Function
Now that we understand it, what is this other evilish variation going to output?
funs = []
for i in range(4):
funs.append(lambda: i)
def show():
for i in funs:
print(i())
show()
What’s the output?The output generated by the code above is:3 3 3 3
The
i
insideshow
is a local variable, so it does not replace the value of the globali
, hence it returns the last value of the globali
, which is 3.
Variation 2 - Define And Show Functions
What about?
funs = []
def define():
for i in range(4):
funs.append(lambda: i)
def show():
for i in funs:
print(i())
define()
show()
What’s the output?The output generated by the code above is:3 3 3 3
Quite similar to the previous example,
i
inshow
is local and does not overwrite the value of thei
indefine
. Unlike in the previous example, thei
used in the lambda functions is local todefine
instead of being global. For practical purposes, this makes no difference; however, from Python’s perspective, lambda functions are implemented with a Closure. This can be verified with the followingshow
function.def show(): for i in funs: print(i()) assert i.__closure__ is not None
In previous examples, a closure was not needed (Python `__closure__` Dunder was
None
) as thei
variable was in the global context; now thati
is local todefine
, Python somehow needs to keep a reference to it in order to get its value when the lambda functions are executed.
Fixing the Issue
How can we modify the lambda function to return the value of i
at the time of its definition, allowing us to obtain values from 0 to 3 when calling the functions in funs
sequentially?
One solution is to add a parameter k
, to the lambda function that defaults to the value of i
at that iteration.
funs = []
for i in range(4):
funs.append(lambda k=i: k)
for i in funs:
print(i())
# Output:
# 0
# 1
# 2
# 3
A function like lambda i=i: i
would also work, but makes it a bit more difficult to understand.