Callback function tkinter button with variable parameter
from tkinter import *
F=Tk()
i=1
while i<10:
newButton = Button(F,text="Show Number",command=lambda:showNumber(i))
newButton.pack(side=TOP)
i+=1
def showNumber(nb):
print(nb)
F.mainloop()
All buttons return 10. Why ?
I want button 1 return 1, button 2 return 2...
Thank you very much for helping me
Solution 1:
Your anonymous lambda
functions are can be though of as closures (as @abernert points out, they're not actually closures in Python's case) - they "close over" the variable i
, to reference it later. However, they don't look up the value at the time of definition, but rather at the time of calling, which is some time after the entire while
loop is over (at which point, i
is equal to 10).
To fix this, you need to re-bind the value of i
to a something else for the lambda to use. You can do this in many ways - here's one:
...
i = 1
while i < 10:
# Give a parameter to the lambda, defaulting to i (function default
# arguments are bound at time of declaration)
newButton = Button(F, text="Show Number",
command=lambda num=i: showNumber(num))
...
Solution 2:
This is explained in the Python FAQ: Why do lambdas defined in a loop with different values all return the same result?.
Quoting the FAQ answer:
This happens because x is not local to the lambdas, but is defined in the outer scope, and it is accessed when the lambda is called — not when it is defined…
In order to avoid this, you need to save the values in variables local to the lambdas, so that they don’t rely on the value of the global…
In other words, your new functions aren't storing the value of i
, they're storing the variable i
. And they're all storing the same variable i
, which has the value 10
at the end of your loop. In fact, if you add an i = 'spam'
right before F.mainloop()
, you'll see that all the buttons now print out the string spam
instead of a number.
This is very useful when you're trying to create closures—functions that can affect their defining environment.* But when you're not trying to do so, that can get in the way.
The simplest way around this is to use a parameter with a default value. Default values don't hold variables; just values, which are evaluated at the time the function is defined. So:
newButton = Button(F,text="Show Number", command=lambda num=i: showNumber(num))
* Note that in this case, there aren't actually any closures involved, because i
is a global, rather than a local in the enclosing scope. But really, this is just because Python has special handling for globals and doesn't need a closure here; conceptually, if you think of there being one, you won't get into any trouble unless you start looking at the __closure__
or __code__
attributes.