Tkinter Commands: Function arguments in lambdas? - python

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?

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), []]
Explanation
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

Related

How to bind <Enter> and <Leave> to a looping declaration in tkinter python

So I've got this simple demo for my problem:
from functools import partial
import tkinter as tk
root = tk.Tk()
root.title("Test")
root.geometry("400x400")
colors = ["red", "green", "blue"]
for x in range(3):
firstFrame = tk.Frame(root, bg=colors[x])
firstFrame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
firstFrame.bind("<Enter>", lambda e: print(f"Enter: {x}"))
firstFrame.bind("<Leave>", lambda e: partial(print, f"Leave: {x}")())
root.mainloop()
When I run the code and hover over the red section, then the green one and finally over the blue one before closing the window, I get this output:
Enter: 2
Leave: 2
Enter: 2
Leave: 2
Enter: 2
Leave: 2
What I need is:
Enter: 0
Leave: 0
Enter: 1
Leave: 1
Enter: 2
Leave: 2
As you can see I've tried multiple approaches to get the desired output, like using partial to create a whole new function instead of simply calling print, but nothing yielded any results.
This is a common problem in Python lambdas. A for loop variable only exists once, so if a lambda is created inside of a for loop, then the lambda will capture the changing, mutable value of the variable.
The trick to get around this is to exploit Python's default arguments. When a default argument is created for a function, it effectively creates a new property on that function object storing the current value of the variable. This is a separate variable, so future reassignments of the loop variable will not affect the default argument's value.
firstFrame.bind("<Enter>", lambda e, arg=x: print(f"Enter: {arg}"))
You'll see people name the default argument the same thing as the enclosing variable. The following snippet is exactly equivalent to the former; the x argument to the lambda shadows the x argument from the for loop.
firstFrame.bind("<Enter>", lambda e, x=x: print(f"Enter: {x}"))
This just reduces the cognitive load for the programmer to switch between two variable names for what is, conceptually, the same variable.

Creating lambdas iteractively in Python: why does this approach work but the other one doesn't?

I've been trying to create a bunch of lambdas, one for each key in a dictionary, without having to do them one by one. I ended up achieving what I wanted to, but I want to understand why my first approach didn't work, while the second one did. I assumed they would produce the exact same results... I don't see what I'm missing!
I've included a small reprex below:
# approach 1 ========================================
bunch_of_funcs = {
"func1": None,
"func2": None,
"func3": None,
"func4": None,
}
for func_name in bunch_of_funcs:
bunch_of_funcs[func_name] = lambda: print(func_name)
# now executing... prints func4 4 times
for func in bunch_of_funcs.values():
func()
# approach 2 ========================================
def lambda_func(func_name):
return lambda: print(func_name)
for func_name in bunch_of_funcs:
bunch_of_funcs[func_name] = lambda_func(func_name)
# now executing... prints what i expect
for func in bunch_of_funcs.values():
func()
EDIT: After chepner's answer, I tried "localizing" the variable by wrapping the for loop inside of a function. I'm still getting the same result, though... Where is this global variable being created? And why is it being changed on every iteration?
My change below:
def make_lambdas(dict_of_funcs):
for func_name in dict_of_funcs:
dict_of_funcs[func_name] = lambda: print(func_name)
make_lambdas(bunch_of_funcs)
In both cases, your lambda-defined function wraps a name func_name. However, that name refers to two different variables. In the first case, it refers to a single global variable. In the second case, it refers to 4 different local variables, a new one for each call to lambda_func.

Call many python functions from a module by looping through a list of function names and making them variables

I have three similar functions in tld_list.py. I am working out of mainBase.py file.
I am trying to create a variable string which will call the appropriate function by looping through the list of all functions. My code reads from a list of function names, iterates through the list and running the function on each iteration. Each function returns 10 pieces of information from separate websites
I have tried 2 variations annotated as Option A and Option B below
# This is mainBase.py
import tld_list # I use this in conjunction with Option A
from tld_list import * # I use this with Option B
functionList = ["functionA", "functionB", "functionC"]
tldIterator = 0
while tldIterator < len(functionList):
# This will determine which function is called first
# In the first case, the function is functionA
currentFunction = str(functionList[tldIterator])
Option A
currentFunction = "tld_list." + currentFunction
websiteName = currentFunction(x, y)
print(websiteName[1]
print(websiteName[2]
...
print(websiteName[10]
Option B
websiteName = currentFunction(x, y)
print(websiteName[1]
print(websiteName[2]
...
print(websiteName[10]
Even though it is not seen, I continue to loop through the iteration by ending each loop with tldIterator += 1
Both options fail for the same reason stating TypeError: 'str' object is not callable
I am wondering what I am doing wrong, or if it is even possible to call a function in a loop with a variable
You have the function names but what you really want are the function objects bound to those names in tld_list. Since function names are attributes of the module, getattr does the job. Also, it seems like list iteration rather than keeping track of your own tldIterator index would suffice.
import tld_list
function_names = ["functionA", "functionB", "functionC"]
functions = [getattr(tld_list, name) for name in function_names]
for fctn in functions:
website_name = fctn(x,y)
You can create a dictionary to provide a name to function conversion:
def funcA(...): pass
def funcB(...): pass
def funcC(...): pass
func_find = {"Huey": funcA, "Dewey": funcB, "Louie": FuncC}
Then you can call them, e.g.
result = func_find["Huey"](...)
You should avoid this type of code. Try using if's, or references instead. But you can try:
websiteName = exec('{}(x, y)'.format(currentFunction))

Python default params confusion

I just started learning Python and I'm confused about this example:
def append_to(element, to=None):
if to is None:
to = []
to.append(element)
return to
If to was initialized once, wouldn't to not be None the 2nd time it's called? I know the code above works but can't wrap my head around this "initialized once" description.
def append_to(element, to=None):
to = ...
to here becomes a local variable and is assigned to a list, and is deallocated if you don't assign return value to another variable.
If you want to staying alive for subsequent calls to append_to you should do:
def append_to(element, to=[]):
to.append(element)
return to
Demo:
>>> lst = append_to(5)
>>> append_to(6)
>>> append_to(7)
>>> print lst
[5, 6, 7]
If "to" was initialized once, wouldn't "to" won't be "None" the 2nd time it's called?
to would become None if you don't pass a value for it: append_to(1) and only when to is None will your code rebind the local name to to a newly created list inside the body of your function: to = [].
The default values of functions are assigned only once, that's whatever you assign as a default value, that object will be used for every call you make to the function and will not change, typically the same reference of the default value will be used for every call you make to the function. This matters when you assign mutables as defaults:
l = []
def defArgs(d=l) # default arguments, same default list for every call
d.append(1)
return d
defArgs() is l # Object identity test: True
Run the above function multiple times and you will observe the list growing with more elements because you get only one single copy of argument defaults for every function shared by every function call. But notice here:
def localVars(d=None):
if d is None:
d = [] # Different list for every call when d is None
d = [] is executed every time you call localVars; when the function finishes its job, every local variable is garbage-collected when reference count drops to 0, but not the default values of arguments, they live after the execution of the function and are not typically garbage-collected after the execution of the function.
In Python there is no declaration and initialization stage when you use a variable. Instead, all assignment is a definition where you bind an instance to the variable name.
The interpreter bind the instance to the default value when instantiate the function. When the default value is a mutable object and you only change its state by its methods than the value will be "shared" between calls.

Trying to get the selected Checkbutton's value of Tkinter, but don't work as expected [duplicate]

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 6 months ago.
Here is my GUI and code shown as below,
expected result is:
one -- print 0
two -- print 1
three -- print 2
However, the program prints "2", no matter which check-box get selected. How can I fix it?
from Tkinter import *
root = Tk()
my_list = ['one', 'two', 'three']
cb_value = []
cb = []
def show_index(idx):
print idx
for idx, each in enumerate(my_list):
cb_value.append(IntVar())
cb.append(Checkbutton(root, text=each, variable=cb_value[idx], command=lambda: show_index(idx)))
cb[idx].pack()
root.mainloop()
thanks!
use lambda idx=idx:show_index(idx) instead.
The "problem" is that python functions only bind the name of a variable in the closure (see https://stackoverflow.com/a/11953356/748858 for instance). They actually look up the value from the appropriate place when the function is called. Your functions are never being called until the loop has finished and at that point, idx has the value of 2. Each of your lambda functions, when called are looking up idx and so they're getting the value of 2. The trick that I show above makes idx a variable local to the function (rather than having it picked up via a closure). Since I do this by using a keyword argument, and keyword arguments are evaluated when the function is created, you always get the proper value.

Categories

Resources