- Today, my friend Laurent was grumping about the “crappy” scipy.optimize.minimize function that did not respect the constraints he provided with lambda functions. This is how I re-discovered the consequences of one of the funniest features in Python: the closures of lambda functions, more specifically if defined by a generator expression.
First, a little Python riddle.
What is the value of this?
[lambda x: x + i + 1
for i in range(24, 42)][0](0)
Do you think the answer is 25? Any sane person would.
Well, you are wrong. Obviously the answer is 42, as always. Wait . . . wat?
For the moment, let’s do almost the same in a simpler way:
l = []
for i in range(24, 42):
l.append(lambda x: x + i + 1)
print(l[0](0))
And it prints the numbers from 25 to 42. Now, try:
>>> i = 4242
>>> print(l[0](0))
4242
Now it is obvious: the lambda uses the value of i
of the scope where it is defined.
You can do funny things by creating the lambda in the scope of a function with a local, nonlocal or global i
.
Let’s go back to our problem:
>>> [lambda x: x + i + 1 for i in range(24, 42)][0]
<function <listcomp>.<lambda>>
The <listcomp>
means that the list comprehension defines its own closure. And in this closure, all the lambda functions take the same value of i
as it changes. Note that if you use Python 2, the variable i
is even accessible from the global context as the list comprehension does not define any closure (but generator expressions do, so it’ll work with tuples, sets or dictionaries).
Now, you have some solutions depending on the type of user you are.
- The standard library scout:
from functools import partial
from operator import add
[partial(add, i + 1) for i in range(24, 42)][0](0)
- The Guido fanboy (even if Guido is known for disliking
lambda
):
[(lambda i: lambda x: x + i + 1)(i)
for i in range(24, 42)][0](0)
- The “think out of the box” guy :
[eval('lambda x: x + i + 1', {'i': i})
for i in range(24, 42)][0](0)
EDIT (16/01/21): my friend Jules suggested me yet another way that I think is elegant.
[lambda x, i=i: x + i + 1
for i in range(24, 42)][0](0)
Basically, the lambda function takes a second argument that is provided with a default value.
Thus, since the left-hand side i
is a function argument, it is treated as a local variable.
To be more explicit to the reader, the following example works the same:
[lambda x, j=i: x + j + 1
for i in range(24, 42)][0](0)
I like this method because it reminds me of lambda captures in C++.