0

Running into an issue while working with tkinter, here's a simple example of what I'm doing

import tkinter as tk
from tkinter import ttk
from typing import Any, Callable, Mapping, NamedTuple, Type


values = {
        'eat': ('something', 'nice'),
        'dirt': ('something', 'else'),
        'kid': ('something'), '': tuple()
    }

class MyTuple(NamedTuple):
    widget: Type[tk.Widget]
    config: Mapping[str, Any] = {}
    postcommand: Callable[[ttk.Combobox], None] = None


class Reg():
    def __init__(self, var):
        self.var = var
        self.widgets = (
                MyTuple(ttk.Label),
                MyTuple(ttk.Combobox, {'values': tuple(values.keys()), 'textvariable': self.var}),
                MyTuple(ttk.Combobox, postcommand=self.cb),
                MyTuple(ttk.Combobox, postcommand=self.other_cb),
            )

    def display(self, frame: tk.Tk):
        for w in self.widgets:
            widget = w.widget(frame, **w.config)
            widget.pack()
            if not w.postcommand is None:
                widget['postcommand'] = lambda widget=widget : w.postcommand(widget)

    def cb(self, combobox: ttk.Combobox):
        combobox['values'] = values[self.var.get()]

    def other_cb(self, combobox: ttk.Combobox):
        combobox['values'] = tuple(values.keys())


class GUI(tk.Tk):
    def __init__(self):
        super().__init__()
        v = tk.StringVar()
        reg = Reg(v)
        reg.display(self)

_ = GUI()
_.mainloop()

When running this, it seems that w gets overwritten and the same postcommand is set for all of the comboboxes. I tried changing display to look like this:

from copy import deepcopy

...

    def display(self, frame: tk.Tk):
        for w in self.widgets:
            widget = w.widget(frame, **w.config)
            widget.pack()
            if not w.postcommand is None:
                command = deepcopy(w.postcommand)
                widget['postcommand'] = lambda widget=widget : command(widget)

And I get the error TypeError: cannot pickle '_tkinter.tkapp' object.

Is there an alternative to deepcopy that I can use? All I could find was a way to overwrite the pickle function, but I can't figure out how to apply that here, since I can't even figure out what tkinter object is part of the deepcopy.

Alternatively, is there a way to fix the scoping issue with w?

(Using python 3.8.10)

jade_tmv
  • 3
  • 2

1 Answers1

0

The lambda that calls w.postcommand is not capturing the correct value of w. What happens if you change the lambda inside your loop to this?

widget['postcommand'] = lambda widget=widget, w=w: w.postcommand(widget)

Further reading: What do lambda function closures capture?

cs95
  • 379,657
  • 97
  • 704
  • 746
  • Yes that works! Can you explain or share a resource on why that was how it works? I've used lambdas referring to a local `self.function_name` a lot and never run into something similar. – jade_tmv Apr 11 '23 at 19:40
  • @jade_tmv I added a link in my answer that explains how lambda closures work in python. essentially the `w` is lazily evaluated to be the final value of w after the loop finishes executing, unless you use `w=w` to fix the value to the current loop variable – cs95 Apr 11 '23 at 19:44
  • @cs95 just *function closures* really. It is important to understand, the functions created by lambda expressions work the same as any other function. – juanpa.arrivillaga Apr 11 '23 at 19:50