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

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 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))
    ...

Method 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.


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x