0

I'm trying to make a simple interface with 3 buttons, and each button should trigger an action based on its label. However, even though (I think) I'm passing the correct argument, it always passes the label of the last button. Here's a stripped-down version to show what's happening:

import tkinter as tk
import random

class Application(tk.Frame):

    def __init__(self, window=None):
        super().__init__(window)
        self.labels = ['Washington','London','Paris','Rome','Berlin','Madrid']
        self.buttons = [tk.Button(self),tk.Button(self),tk.Button(self)]
        self.pack()

        for k,button in enumerate(self.buttons):
            button.config(width=10)
            button.grid(row=0, column=k)

        self.update_buttons()


    def update_buttons(self):
        labels = list(random.sample(self.labels,3))
        random.shuffle(labels)

        for label,button in zip(labels,self.buttons):
            button["text"] = label
            button["command"] = lambda: self.verify(label)


    def verify(self, label):
        print(f'You pressed the button with label {label}')
        self.update_buttons()


window = tk.Tk()
app = Application(window=window)
app.mainloop()

Why?

Ziofil
  • 1,815
  • 1
  • 20
  • 30

2 Answers2

2

You are encountering a (late biding) closure problem.

When you create a function using lambda, a closure is created. This means the variables in function's body are looked up at the time you call the lambda, not when you create it (and the scope in which lambda was created will contain variable with the final value that was assigned).

In order to prevent this, you need to create an argument and set it to a default value, which stores the current value of the variable at creation time.

import tkinter as tk
import random

class Application(tk.Frame):

    def __init__(self, window=None):
        super().__init__(window)
        self.labels = ['Washington','London','Paris','Rome','Berlin','Madrid']
        self.buttons = [tk.Button(self),tk.Button(self),tk.Button(self)]
        self.pack()

        for k,button in enumerate(self.buttons):
            button.config(width=10)
            button.grid(row=0, column=k)

        self.update_buttons()


    def update_buttons(self):
        labels = list(random.sample(self.labels,3))
        random.shuffle(labels)

        for label,button in zip(labels,self.buttons):
            button["text"] = label
            button["command"] = lambda label=label: self.verify(label) # Here


    def verify(self, label):
        print(f'You pressed the button with label {label}')
        self.update_buttons()


window = tk.Tk()
app = Application(window=window)
app.mainloop()

You can also use functools.partial, which looks cleaner in my opinion:

import tkinter as tk
import random

from functools import partial

class Application(tk.Frame):

    def __init__(self, window=None):
        super().__init__(window)
        self.labels = ['Washington','London','Paris','Rome','Berlin','Madrid']
        self.buttons = [tk.Button(self),tk.Button(self),tk.Button(self)]
        self.pack()

        for k,button in enumerate(self.buttons):
            button.config(width=10)
            button.grid(row=0, column=k)

        self.update_buttons()


    def update_buttons(self):
        labels = list(random.sample(self.labels,3))
        random.shuffle(labels)

        for label,button in zip(labels,self.buttons):
            button["text"] = label
            button["command"] = partial(self.verify, label)


    def verify(self, label):
        print(f'You pressed the button with label {label}')
        self.update_buttons()


window = tk.Tk()
app = Application(window=window)
app.mainloop()
Božo Stojković
  • 2,893
  • 1
  • 27
  • 52
1

You need to assign a parameter to the lambda function, and pass it as argument to the function.

import tkinter as tk
import random

class Application(tk.Frame):

    def __init__(self, window=None):
        super().__init__(window)
        self.labels = ['Washington','London','Paris','Rome','Berlin','Madrid']
        self.buttons = [tk.Button(self),tk.Button(self),tk.Button(self)]
        self.pack()

        for k,button in enumerate(self.buttons):
            button.config(width=10)
            button.grid(row=0, column=k)

        self.update_buttons()


    def update_buttons(self):
        labels = list(random.sample(self.labels,3))
        random.shuffle(labels)

        for label,button in zip(labels,self.buttons):
            button["text"] = label
            button["command"] = lambda lbl=label: self.verify(lbl)   # <-- here


    def verify(self, label):
        print(f'You pressed the button with label {label}', flush=True)  # <-- added flush=True to ensure the printing is done as the moment you click
        self.update_buttons()


window = tk.Tk()
app = Application(window=window)
app.mainloop()
Reblochon Masque
  • 35,405
  • 10
  • 55
  • 80