Deciphering a lambda expression [duplicate] - python

This question already has answers here:
tkinter creating buttons in for loop passing command arguments
(3 answers)
Closed 6 months ago.
I have been following closely, a YouTube video in order to create an on-screen keyboard using tkinter.
I understand most of what is going on; create our buttons within a list and loop through those buttons, inserting rows at the best position to create a good looking keyboard.
The only problem I've been having with this task, is when I click a button, the text of the button gets inserted into an Entry box via tkinter.
The way in which we do this, is to assign a command to the button, and upon pressing it, calls an anonymous function, assigning the button that we pressed to the 'x' parameter. We then pass this parameter to another method and insert the value into the Entry widget.
I'm trying to understand, why can't we just pass in the button itself, but instead we have to assign the button to a parameter...
self.textbox = textbox
row=0
col=0
buttons = [
'1','2','3','4','5','6','7','8','9','0',
'q','w','e','r','t','y','u','i','o','p',
'a','s','d','f','g','h','j','k','l',
'z','x','c','v','b','n','m','end']
for button in buttons:
# why not the following command instead?
# command = lambda: self.command(button)
command = lambda x=button: self.callback(x)
if button != 'end':
tk.Button(self, text=button, width=5,
command=command).grid(row=row, column=col)
col+=1
if col > 9:
col=0
row+=1
def command(self, button):
x = button
self.callback(x)
def callback(self, value):
self.textbox.insert(tk.END, value)
With the above code, I can successfully insert the desired value into my entry widget. However, if we use the code I have commented out, it will instead insert 'end' to the entry widget.
I have tried to replicate the lambda function into a separate method, but I am still inserting an 'end' into my entry widget.
# using the commented code above and passing in button as the parameter
def command(self, button):
x = button
self.callback(x)
def callback(self, value):
self.textbox.insert(tk.END, value)
I thought If I was able to replicate the function into a non-anon function, then it would work, but clearly it is not.
What exactly is my lambda function doing it and is there a way to replicate it by using a non-anonymous function (ie. method)?

The problem with
for button in buttons:
command = lambda: self.command(button)
is that command is equal to lambda: self.command(button), which is passed to the Button constructor.
So the command of the Button instance is equal to lambda: self.command(button).
You would think that self.command(button) is evaluated, and replaced by the result of the following:
def command(self, button):
x = button
self.callback(x)
However, there is no reason to do so: since self.command(button) is inside of the lambda function, it will evaluated when the lambda will be called.
As a consequence, when executing a so created lambda function, button will be evaluated to the last value it was assigned, not to the value it had when the lambda was created.
Therefore, after that loop, all of those lamda functions will have the same behaviour, and the button inside of their body will point toward the same Button instance.
If you want to have a method instead of a lambda, you can wrap the command method inside of a command builder:
def build_command(self, button):
def command():
self.callback(button)
return command
Then just call that method to get the expected command:
command = self.build_command(button)
The reason why this works, is that button is then a variable local to build_command, and the inner command "inherits" this variable into its own local variable dictionary (given by locals()).
This local dictionary will stay unaffected by outer changes.

Here is a sample code that shows what Right leg is explaining. Python lazily evaluates number:
functions1 = []
functions2 = []
for number in range(0,5):
functions1.append(lambda x=number: print(x))
functions2.append(lambda : print(number))
for function in functions1:
function()
for function in functions2:
function()
Because of that the functions2 only prints 4. While lambda x=number evaluates number immediately, and set its value to x.

Related

Can you bind a button as well as a key press to a function in tkinter?

I am trying to make a small program where a user can click a button and it prints text, but if the user clicks the space bar it will also print the text. I know how to connect a button and a function using "command=....." not sure how to bind keys though. any help would be appreciated.
import tkinter as tk
root = tk.Tk()
def yes():
print("yes")
okbtn = tk.Button(text='OK', font=("Helvetica",50), bg = "red", command=yes, width=10, height=3)
okbtn.pack()
root.mainloop()
You can bind functions to keys, using the .bind method which takes a key (or combination of modifiers and keys) and a function to execute
So in your example, adding the line below, would bind the yes function to the spacebar
root.bind('<space>', lambda event: yes())
Note that any bound function will take a tkinter event as argument (which contains mouse coordinates, time of execution, reference to master widget, and more) - I have ignored the event argument in this case, by making a dummy lambda. However, it can often be useful
Here is an example of a function where the event is actually being used (prints the mouse position at the time where the function was called)
def motion(event):
print("Mouse position: (%s %s)" % (event.x, event.y))
You can check out this link for more information about even binding in tkinter https://www.pythontutorial.net/tkinter/tkinter-event-binding/

Label appearing conditionally

I am trying to make a label appear if the condition of my entry (textbox) is met. Unfortunately I cannot see anything when I am pressing the button on the testing. Here is what I have:
from tkinter import *
main= Tk()
firstname=Entry(main).place(x=30, y=50)
def register():
if any(i.isdigit() for i in firstname.get())== True:
print (Label(main,text='no numbers please').place(x=30, y=180))
else:
print(Label(main, text='pass').place(x=40, y=170))
register=Button(main,text='REGISTER', command= lambda :register).place(x=300, y=200)
There are at least three problems with your code. The first is in how you define the button's command:
register=Button(main,text='REGISTER', command= lambda :register)
When you do command=lambda: register, you're telling the button "when you're clicked run the code register". register all by itself does nothing. Since register is (supposed to be) a function, you need to call it like register() inside the lambda.
Since you aren't passing any values to the function, the lambda is completely unnecessary. Instead, just directly reference the function: command=register without the parenthesis.
The second problem is that you've used the name register to be two different things: a function and a reference to a widget. Because of the ordering of the code, command=register or command=lambda: register() will try to call the button rather than the function.
The third problem is a very, very common mistake. In python, when you do x = y().z(), x is given the value of z(). Thus, register = Button(...).pack(...) returns the value of pack(...) and pack (and grid and place) always returns None.
Therefore, you've set register to None, and when you try to call it you get NoneType object is not callable.
In addition to fixing the command, you need to pick a different name for either the function or the button. And you should not be calling place (or pack or grid) in-line with creating the widget. They should be separate steps.
So, putting that all together, you need to define firstname like this so that firstname is not None:
firstname=Entry(main)
firstname.place(x=30, y=50)
And then you need to define the button like this:
register_button = Button(main,text='REGISTER', command= register)
register_button.place(x=300, y=200)

receiving selected choice on Tkinter listbox

I'm trying to build a listbox using Tkinter and receive the selected option by clicking it.
import Tkinter as tk
from Tkinter import *
root = tk.Tk()
lst=Listbox(root, height=30, width=50)
lst.insert(1, "hy")
lst.insert(2, "hello")
lst.insert(3, "hey")
lst.pack()
sel = lst.curselection()
print sel
root.mainloop()
However, when I run the code it prints me an empty tuple before I pressed any choise.
Does someone know how to get the selected choise after I press one and not right after I run it?
Thanks a lot :)
You are getting the selection about a millisecond after creating the widget, well before the user has a chance to see the UI much less interact with it.
GUI programs are event based, meaning that things happen in response to events. Events are things like clicking buttons, inserting data into input widgets, and selecting items from listboxes.
You need to do one of two things: create a button or other widget which will get the selected item, or configure it so that a function is called whenever an item is selected.
No matter which solution you use, you will need a function that ultimately calls the curselection method of the listbox to get a list of indices. You can then call the get method to get the selected item or items.
Here's a function definition that will print the selected item, or print "no selection" if nothing is selected. So that it can be resused without modification. we'll define it to take the listbox as an argument.
Note: this example assumes the widget only supports a single select, to keep it simple:
def print_selection(listbox):
selection = listbox.curselection()
if selection:
print(f"selected item: {listbox.get(selection[0])}")
else:
print("nothing is selected")
Using a button
To call this from a button is straight-forward. We just create a button after we create the listbox, and use the command attribute to call the function. Since the function we wrote earlier needs a parameter, we'll use lambda to create a temporary function for the button.
button = tk.Button(root, text="Print Selected Item", command=lambda: print_selection(lst))
button.pack()
Calling the function when the selection is made
To call the function whenever the user changes the selection, we can bind a function to the <<ListboxSelect>> event. We'll create a separate function for this, and then pull the widget from the event object that is automatically passed to the function.
def print_callback(event):
print_selection(event.widget)
lst.bind("<<ListboxSelect>>", print_callback)
First of all, the reason you are getting an empty tuple is because you have executed the statements:
sel = lst.curselection()
print(sel)
before you have executed the root.mainloop()
Secondly, your setup for listbox fails to include a StringVar variable to hold your list.
Once the variable has been defined, you should be able to use the .insert statements to add your list items one at a time, or you can initialize the StringVar variable using a .set('hy', 'hello', 'hey') command.
To provide a return of a selected variable, you must incorporate an event handler to determine the list position selected onclick or some other triggering method.
For a pretty clear explanation of these characteristics check here

How to do the callback of tk.Variable inside a list

I'm using a list of tk Variables in my GUI. How to make the callback use the i-Value of the traced tk.Variable?
def callback(a,b,c):
print 'i+2'
ButtonsList=[]
VarList=[]
i=0
while i<30:
VarList.append(tk.BooleanVar())
VarList[i].trace('w',callback)
ButtonsList.append(tk.Checkbutton(root, text="This is a CB",variable=VarList[i]))
ButtonsList[i].place(x=x,y=i*20)
i+=1
You could wrap your callback in a lambda that adds additional arguments to the function call.
def callback(a,b,c,idx):
print 'i+2'
#later on in the program:
VarList[i].trace('w', lambda a,b,c,i=i: callback(a,b,c,i))
Note the i=i in the lambda. This is necessary for variables whose value changes after you register the callback. Without it, i would always be 30, regardless of which Checkbutton you click.

Python tkinter button callback unexpected behaviour [duplicate]

This question already has answers here:
Creating functions (or lambdas) in a loop (or comprehension)
(6 answers)
Closed 6 months ago.
I have made a simple "program launcher" in Python. I have a tab delimited text file, with, at the moment, just:
notepad c:\windows\notepad.exe
write c:\windows\write.exe
The program reads the textfile and creates an array of objects. Each object has a name property (e.g. notepad) and a route property (e.g. C:\windows\notepad.exe). Then, for each object, a button should be made with the correct name on the button, and clicking the button should execute the correct program using the route.
The program very nearly works. Indeed, the array of objects is formed correctly, because the for loop correctly prints out two different program names, and two different routes. The problem is that both buttons, although labeled correctly, launch the write program ! I believe the problem is arising somewhere in the callback, but my Python knowledge just isn't developed enough to solve this! As you can see from my code below, I have tried an "inline" callback, and with a "runprog" function defined. They both give the same outcome.
Your help would be appreciated.
import Tkinter as tk
import subprocess
class MyClass:
def __init__(self, thename,theroute):
self.thename=thename
self.theroute=theroute
myprogs = []
myfile = open('progs.txt', 'r')
for line in myfile:
segmentedLine = line.split("\t")
myprogs.append(MyClass(segmentedLine[0],segmentedLine[1]))
myfile.close()
def runprog(progroute):
print(progroute)
subprocess.call([progroute])
root = tk.Tk()
button_list=[]
for prog in myprogs:
print(prog.thename)
print(prog.theroute)
button_list.append(tk.Button(root, text=prog.thename, bg='red', command=lambda: runprog(prog.theroute)))
# button_list.append(tk.Button(root, text=prog.thename, bg='red', command= lambda: subprocess.call(prog.theroute)))
# show buttons
for button in button_list:
button.pack(side='left', padx=10)
root.mainloop()
Change your command to look like this:
tk.Button(..., command=lambda route=prog.theroute: runprog(route))
Notice how the lambda has a keyword argument where you set the default value to the route you want to associate with this button. By giving the keyword arg a default value, you are "binding" this value to this specific lambda.
Another option is to use functools.partial, which many people find a little less intimidating than lambda. With this, your button would look like this:
import functools
...
tk.Button(..., command=functools.partial(runprog,route)
A third option is to move the "runprog" function to the class instead of in the main part of your program. In that case the problem becomes much simpler because each button is tied specifically to a unique object.
tk.Button(..., command=prog.runprog)
Just change this line:
button_list.append(tk.Button(root, text=prog.thename, bg='red', command=lambda: runprog(prog.theroute)))
to:
button_list.append(tk.Button(root, text=prog.thename, bg='red',
command= (lambda route:(lambda: runprog(route))) (prog.theroute)))
Reasoning: when you create a lambda function (or any other function within a function), it does have access (in Python 2, read-only access) to the variables in the outer function scope. However, it does access the "live" variable in that scope - when the lambda is called, the value retrieved from "prog" will be whatever "prog" means at that time, which in this case will be the last "prog" on your list (since the user will only click a button long after the whole interface is built)
This change introduces an intermediary scope - another function body into which the current "prog" value is passed - and prog.theroute is assigned to the "route" variable in the moment the expression is run. That is done once for each program in the list. The inner lambda which is the actual callback does use the "route" variable in the intermediate scope - which holds a different value for each pass of the loop.

Categories