4

Consider the following Python 3 instructions

res = []
for a in range(3) :
    res.append(lambda n : n + a)

with the aim of building a list res of three functions res[0], res[1], and res[2] such that res[i](n) returns n + i, for all i in [0, 1, 2].

Nevertheless, one obtains

>>> res[0](0)
2

instead of

>>> res[0](0)
0

One also have

>>> res[1](2)
4

The explanation of this behavior is that in the expression n + a in the body of any dynamically generated anonymous function of the example, the symbol a is not evaluated at the creation of the function. The evaluation is performed at the exit of the for statement, explaining why all functions res[0], res[1], and res[2] return the value of their argument plus 2è (becauseabrowsesrange(3)and2` is its last value).

Notice that the problem does not lie in the fact that we use anonymous functions. Indeed, the instructions

res = []
for a in range(3) :
    def f(n) :
        return n + a
    res.append(f)

lead to the same behavior.

Notice also that we can meet the objective set out above by using the function eval of Python:

res = []
for a in range(3) :
    s = "lambda n : n + %s" %(a)
    res.append(eval(s))

The trick lies on the fact that the dynamic value of a is considered for the creation of each function of res.

Now my questions are

  1. is this a bug or a feature?
  2. is there an other way not passing by eval to obtain the expected behavior?
Samuele Giraudo
  • 409
  • 2
  • 14
  • Does this answer your question? [What do (lambda) function closures capture?](https://stackoverflow.com/questions/2295290/what-do-lambda-function-closures-capture) – Georgy Aug 01 '20 at 11:52

4 Answers4

3

Because of closure the dynamic functions referencing variable a and using the final value it had at the end of the for loop when they execute.

You can prevent that by making it an function argument with a default value so it doesn't need to be provided on calls. The value of a at the time each dynamic function is defined will become the value used. This is what I mean:

res = []
for a in range(3) :
    res.append(lambda n, a=a: n + a)

print(res[0](0))  # -> 0
print(res[1](2))  # -> 3
martineau
  • 119,623
  • 25
  • 170
  • 301
  • That makes it a two-argument function, though. Not so clean. – Stefan Pochmann May 06 '15 at 20:20
  • @StefanPochmann: Not really, as illustrated by the sample calls to the functions at the end. This is because values for arguments with default values are optional. – martineau May 06 '15 at 20:44
  • Yes, it's optional, but it's still there. – Stefan Pochmann May 06 '15 at 20:49
  • @StefanPochmann: You're entitled to your own minority opinion, of course, but this is a common Python idiom to deal with issues like this and is very lightweight. – martineau May 06 '15 at 21:03
  • Ok, thanks, I tested it and it's indeed smaller and faster than mine. I'll keep it in mind. – Stefan Pochmann May 06 '15 at 22:02
  • Just one more thing: You say his functions are using the final value that `a` had at the end of the loop. While technically correct, I think this is misleading. They're only using that value because that's still the value `a` has when they're called. If you increase `a` further, even after having used the functions after the loop, then the functions use the new value, not the one it had at the end of the loop. – Stefan Pochmann May 06 '15 at 22:07
  • @StefanPochmann: That's exactly why I updated my answer to say that the dynamic functions _refer_ to the `a` variable...implying that they will pick up whatever value it has each time they're called. – martineau May 07 '15 at 03:21
  • I just think many won't get that but just understand and remember the *"the value it had at the end of the loop"* part. – Stefan Pochmann May 07 '15 at 15:11
  • @StefanPochmann: Well, if they follow the "not so clean" suggestion in my answer, it won't matter what value the loop variable has except when each function is defined. – martineau May 07 '15 at 16:48
2

I cant speak to #1 ... except to say I am sure that is intentional and desirable behaviour for some definition of intentional and desirable

but WRT #2

res = []
for a in range(3) :
    res.append(lambda n,b=a : n + b)

res[0](0)

this will evaluate a as the default 2nd argument while inside the for loop(instead of after exiting) ...

of coarse this leaves it open to something like

res[0](0,8)
Joran Beasley
  • 110,522
  • 12
  • 160
  • 179
0

This would also work, if you don't want to use lambda:

>>> from functools import partial
>>> for a in range(3) :
    def f(a, n):
        return n + a
    f = partial(f, a)
    res.append(f)

>>> res[0](0)
0
>>> res[1](2)
3
laike9m
  • 18,344
  • 20
  • 107
  • 140
0

This gives you clean functions which just have one argument:

res = []
for a in range(3) :
    res.append((lambda a: lambda n: n+a)(a))

Might be better for readability and efficiency to do it this way, though:

def adder(amount):
    return lambda n: n + amount

res = []
for a in range(3) :
    res.append(adder(a))

Also, your "The evaluation is performed at the exit of the for statement" is wrong. Try printing after the loop, then increase a further, then print again. You'll see that the functions now use the further increased value of a (only with your version, of course, not with mine). That's because your functions don't have their own a but use the same global a, and evaluate it whenever they're called.

Stefan Pochmann
  • 27,593
  • 8
  • 44
  • 107