1

Given this piece of code(python)

s = [None]*10
def func():
    for i in range(10):
        def update():
            print i,
        s[i] = update

func()
for i in range(10):
    s[i]()

why this result is ten 9s, instead of 0,1,2,3,...9 ?

btw, I've also print s[0]~s[9],which are 10 function addresses, and there are different from each other.

Junjie
  • 469
  • 1
  • 4
  • 14
  • Actually, the background is I have some widgets stored in a list. I need to give each widget a callback function, written like the above code. and I found all widgets' callback function is same as the last one's. – Junjie Aug 15 '12 at 05:50

3 Answers3

3

You've created a bunch of closures over i, but they are all sharing the same (final) value of i

You need to make a tiny modification like this

>>> s = [None]*10
>>> def func():
...     for i in range(10):
...         def update(i=i):
...             print i,
...         s[i] = update
... 
>>> func()
>>> for i in range(10):
...     s[i]()
... 
0 1 2 3 4 5 6 7 8 9
John La Rooy
  • 295,403
  • 53
  • 369
  • 502
  • This is a nice way to fix the bug. Thank you – Junjie Aug 15 '12 at 05:56
  • While this is entirely right, I don't think saying "they're closures" is enough to explain why this modification works - if OP understood what closures are and do, they wouldn't have to ask this question. – lvc Aug 15 '12 at 05:59
0

@gnibbler's code will fix yours (so accept his answer if anyone's). As for why it is printing all 9's, look at your update function. You are (again, as @gnibbler mentioned) continuously returning the i that you defined in for i in range(10). Since in your original code you did not call the update method with a function argument, it simply prints the i that was defined in the func scope (a completely different i which will be 9 after the function completes).

To perhaps make it more clear, try changing the i in func to a completely different name - the result will be the same.

RocketDonkey
  • 36,383
  • 7
  • 80
  • 84
0

What's happening is the same as this:

>>> a = 5
>>> def foo(): print(a)

>>> foo()
5
>>> a = 10
>>> foo()
10
>>> a = 'fred'
>>> foo()
fred

And also the same as this:

>>> def bar(): return b

>>> bar()
Traceback (most recent call last):
  File "<pyshell#30>", line 1, in <module>
    bar()
  File "<pyshell#29>", line 1, in bar
    def bar(): return b
NameError: global name 'b' is not defined
>>> b = 3
>>> bar()
3

The variables you used inside a function aren't resolved until the function is called not when it is written. There's some magic, called closures, that means functions defined inside other functions (as your update functions are defined inside func) still have access to all the variables defined in the outer function - but they still don't actually get resolved until the function is called. So, by the time each of your update functions is called, i is 9.

Using default argument values, as in @gnibbler's answer, works because the i that each update looks up will resolve to the argument (which shadows the outer variable). Those won't change, because default argument values are evaluated when the function is defined (which also leads to the mutable defaults bug that a lot of people run into sooner or later).

Community
  • 1
  • 1
lvc
  • 34,233
  • 10
  • 73
  • 98
  • @gnibbler will work since in update, the function's parameter has made a shaddow copy of i. However this will work since i is a interger. What if i is a list or dict, which means when New_i=i, New_i is only a reference to i? – Junjie Aug 15 '12 at 06:05
  • @user894625 indeed, default arguments won't work if `i` is mutable, and your loop mutates it. In that case, you would have to do `update(i=copy(i))`, using `copy` from the [copy module](http://docs.python.org/library/copy.html) (`i=i[:]` will also work for lists, but not necessarily for other things). This *isn't* needed if, for example, you're iterating over a list of lists - only if you're changing the same list and need to capture what it contained at a particular stage. – lvc Aug 15 '12 at 06:13