31

If I make two lists of functions:

def makeFun(i):
    return lambda: i

a = [makeFun(i) for i in range(10)]
b = [lambda: i for i in range(10)]

why do lists a and b not behave in the save way?

For example:

>>> a[2]()
2
>>> b[2]()
9
Akaisteph7
  • 5,034
  • 2
  • 20
  • 43
Anssi
  • 518
  • 4
  • 8
  • See also [python - How do lexical closures work? - Stack Overflow](https://stackoverflow.com/questions/233673/how-do-lexical-closures-work) – user202729 Jun 18 '22 at 09:58

6 Answers6

24

As others have stated, scoping is the problem. Note that you can solve this by adding an extra argument to the lambda expression and assigning it a default value:

>> def makeFun(i): return lambda: i
... 
>>> a = [makeFun(i) for i in range(10)]
>>> b = [lambda: i for i in range(10)]
>>> c = [lambda i=i: i for i in range(10)]  # <-- Observe the use of i=i
>>> a[2](), b[2](), c[2]()
(2, 9, 2)

The result is that i is now explicitly placed in a scope confined to the lambda expression.

Stephan202
  • 59,965
  • 13
  • 127
  • 133
21

Technically, the lambda expression is closed over the i that's visible in the global scope, which is last set to 9. It's the same i being referred to in all 10 lambdas. For example,

i = 13
print b[3]()

In the makeFun function, the lambda closes on the i that's defined when the function is invoked. Those are ten different is.

Jonathan Feinberg
  • 44,698
  • 7
  • 80
  • 103
  • Ok, now this makes sense for me. Thanks! – Anssi Dec 03 '09 at 17:15
  • As of Python 3, list comprehensions have their own scope now. So changing it's value wouldn't change what print(b[3]()) would output. More details about the list comprehension behavior can be found [here](https://stackoverflow.com/questions/4198906/list-comprehension-rebinds-names-even-after-scope-of-comprehension-is-this-righ) – Akaisteph7 Jul 13 '19 at 00:52
7

One set of functions (a) operates on the argument passed and the other (b) operates on a global variable which is then set to 9. Check the disassembly:

>>> import dis
>>> dis.dis(a[2])
  1           0 LOAD_DEREF               0 (i)
              3 RETURN_VALUE
>>> dis.dis(b[2])
  1           0 LOAD_GLOBAL              0 (i)
              3 RETURN_VALUE
>>>
hughdbrown
  • 47,733
  • 20
  • 85
  • 108
3

To add some clarity (at least in my mind)

def makeFun(i): return lambda: i
a = [makeFun(i) for i in range(10)]
b = [lambda: i for i in range(10)]

a uses makeFun(i) which is a function with an argument.

b uses lambda: i which is a function without arguments. The i it uses is very different from the previous

To make a and b equal we can make both functions to use no arguments:

def makeFun(): return lambda: i
a = [makeFun() for i in range(10)]
b = [lambda: i for i in range(10)]

Now both functions use the global i

>>> a[2]()
9
>>> b[2]()
9
>>> i=13
>>> a[2]()
13
>>> b[2]()
13

Or (more useful) make both use one argument:

def makeFun(x): return lambda: x
a = [makeFun(i) for i in range(10)]
b = [lambda x=i: x for i in range(10)]

I deliberately changed i with x where the variable is local. Now:

>>> a[2]()
2
>>> b[2]()
2

Success !

1

Lambdas in python share the variable scope they're created in. In your first case, the scope of the lambda is makeFun's. In your second case, it's the global i, which is 9 because it's a leftover from the loop.

That's what I understand of it anyway...

Virgil Dupras
  • 2,634
  • 20
  • 22
1

Nice catch. The lambda in the list comprehension is seeing the same local i every time.

You can rewrite it as:

a = []
for i in range(10):
    a.append(makefun(i))

b = []
for i in range(10):
    b.append(lambda: i)

with the same result.

Joe Koberg
  • 25,416
  • 6
  • 48
  • 54
  • I'm not sure what you're trying to say. In your example, `b[2]()` also returns 9. – Virgil Dupras Dec 03 '09 at 17:06
  • I'm saying it's equivalent to the list comprehensions; not that a and b and the same. I figured the spelled-out `for` loops would make it easier to see where `i` comes from. – Joe Koberg Dec 03 '09 at 17:31