5

I am hoping the following would produce a list of 3 constant functions taking values 0, 1 and 2:

lis = []
for i in range(3):
    lis.append( lambda: i )

But they all end up taking value 2. I would expect deep copying to fix the problem but it doesn't seem to work.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
user1470575
  • 145
  • 2
  • 5
  • 3
    This is a standard question. e.g. http://stackoverflow.com/questions/1107210/python-lambda-problems, http://stackoverflow.com/questions/1924214/python-lambdas-and-scoping. Many, *many* more. – Chris Morgan Dec 26 '12 at 01:49
  • related: [Why results of map() and list comprehension are different?](http://stackoverflow.com/questions/139819/why-results-of-map-and-list-comprehension-are-different) – jfs Dec 26 '12 at 04:38

2 Answers2

3

@Boud gave a very good answer explaining why your code doesn't work as you expect it to. Baically, you have to evaluate the value of i before it is referenced in lambda. Here is a bit hacky way to do it:

lis = []
for i in range(3):
    lis.append( lambda i=i: i )

This uses Python's default function arguments' values feature, e.g. in a function one would write:

def f(i=10):
    return i

Now, the trick is that an argument has its default value stored at the point when a function (a method, a lambda expression) is created. Thus:

j = 10
def f(i=j):
    return i

j = 20
print( f(125) ) # no matter that j has been changed, the output is...
>>> 125

And the same trick applies to lambda. To make it a bit clearer:

lis = []
for j in range(3):
    lis.append( lambda i=j: i )

# Calling the lambdas
print(lis[1]())
>>> 1
Zaur Nasibov
  • 22,280
  • 12
  • 56
  • 83
1

You loop to avoid writing something like this:

lis.append( lambda: 0 )
lis.append( lambda: 1 )
lis.append( lambda: 2 )

Your intention is to write lambda functions that return constant integers. But you are defining and appending a function that is returning object i. Thus the 3 appended functions are the same.

The byte code behind the functions created returns i:

In [22]: import dis
In [25]: dis.dis(lis[0])
  3           0 LOAD_GLOBAL              0 (i)
              3 RETURN_VALUE        

In [26]: dis.dis(lis[1])
  3           0 LOAD_GLOBAL              0 (i)
              3 RETURN_VALUE        

In [27]: dis.dis(lis[2])
  3           0 LOAD_GLOBAL              0 (i)
              3 RETURN_VALUE   

Calling any of those functions returns the latest value of ithat is 2 in your sample code:

In [28]: lis[0]()
Out[28]: 2

if you delete i object, you get an error:

In [29]: del i

In [30]: lis[0]()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-30-c9e334d64652> in <module>()
----> 1 lis[0]()

<ipython-input-18-15df6d11323a> in <lambda>()
      1 lis = []
      2 for i in range(3):
----> 3     lis.append( lambda: i )

NameError: global name 'i' is not defined

A solution could be to keep using the loop to write the code with the constants you need and actually run that code:

In [31]: lis = []
    ...: for i in range(3):
    ...:     exec 'lis.append( lambda: {} )'.format(i)
    ...:  

With the following results:

In [44]: lis[0]()
Out[44]: 0

In [45]: lis[1]()
Out[45]: 1

In [46]: dis.dis(lis[0])
  1           0 LOAD_CONST               1 (0)
              3 RETURN_VALUE        

In [47]: dis.dis(lis[1])
  1           0 LOAD_CONST               1 (1)
              3 RETURN_VALUE        
Zeugma
  • 31,231
  • 9
  • 69
  • 81
  • 1
    The diagnosis is correct (it's a closure issue) but please DON'T use `exec`! It's scary! Evil! Nasty! Unpleasant! – Chris Morgan Dec 26 '12 at 01:38
  • @ChrisMorgan My only goal here is to try to explain lambda closures another way as it is coming back again and again... `exec` here is to illustrate the meta level of the expected source code (OP's intention) . I agree using default arg in the lambda as per @BasicWolf is the way to go outside of the explainations. – Zeugma Dec 26 '12 at 01:59
  • @ChrisMorgan The only working solution here uses exec. Do you have a better suggestion? – kilojoules Dec 18 '17 at 19:28
  • 1
    @kilojoules: see the answers to the question this question has been marked a duplicate of: https://stackoverflow.com/questions/1924214/python-lambdas-and-scoping – Chris Morgan Dec 19 '17 at 01:23