Reputation: 13
My program has a lambda as a command for a tkinter object inside a loop. I want the lambda to pass an argument for a function where the function will make some changes according to the argument.
for cat in self.t_m_scope:
print(cat)
self.t_m_scopeObj[cat] = [tk.Checkbutton(self, text = cat, command = lambda: self.t_m_checkUpdate(cat)), []]
if self.t_m_scopeVars[cat][0]: self.t_m_scopeObj[cat][0].toggle()
self.t_m_scopeObj[cat][0].grid(row = 5, column = colNum, padx = (10,10), pady = (2,5), sticky = tk.W)
Whenever I try to run this, it always passes in the last iteration of cat because of some weird nature in tkinter commands. Instead, I want it so that the argument matches exactly whatever iteration it was at when the lambda was defined. I don't want the argument to change just because I clicked the check button after its iteration.
Here is the function that the lambda calls.
def t_m_checkUpdate(self, cat):
self.t_m_scopeVars[cat][0] = not self.t_m_scopeVars[cat][0]
for subcat in range(len(self.t_m_scopeVars[cat][1])):
if self.t_m_scopeVars[cat][0] != self.t_m_scopeVars[cat][1][subcat]:
self.t_m_scopeVars[cat][1][subcat] = not self.t_m_scopeVars[cat][1][subcat]
self.t_m_scopeObj[cat][1][subcat].toggle()
As some context to what this program is, I am trying to have a bunch of checkbuttons toggle on and off in response to a click on a main checkbutton. The only way I know which checkbuttons to toggle (since I have many) is to pass an argument to a function using lambda.
I do understand that I could just find the value of the current iteration using if/elif/else and pass a string instead of a variable but I seriously don't want to do that because it's just not scalable.
If it helps, the text for my checkbutton and the argument I want to pass is one and the same.
Is there any workaround so I can preserve the current iteration without Tkinter going all wonky?
Upvotes: 0
Views: 215
Reputation: 2497
You need to bind the data in cat
to the function when it's created. For an explanation, see this question. There are a few ways to do this, including partials and closures.
This is how you would apply a partial to your example:
from functools import partial
for cat in ...:
check_update_cat = partial(self.t_m_checkUpdate, cat)
self.t_m_scopeObj[cat] = [tk.Checkbutton(self, text=cat, command=check_update_cat), []]
In your code, the lambda refers to an iterator variable (cat
) declared in the outer scope of the loop. One thing to note is that the scope of a loop "leaks" the iterator variable, i.e. you can still access it after the loop. The lambda functions refer to the iterator variable by reference, so they access the most recent value of the iterator.
A runnable example:
a = []
for i in range(5):
a.append(lambda: i)
a[0]() # returns 4
a[1]() # also returns 4
del i # if we delete the variable leaked by the for loop
a[0]() # raises NameError, i is not defined because the lambdas refer to i by reference
Instead, we want to bind the value of the iterator to the functions at each step. We need to make a copy of the iterator's value here.
import functools
a = []
for i in range(5):
a.append(functools.partial(lambda x: x, i)) # i is passed by value here
a[0]() # returns 0
a[1]() # returns 1
del i
a[0]() # still returns 0
Upvotes: 1