2

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)?

juiceb0xk
  • 949
  • 3
  • 19
  • 46

2 Answers2

1

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.

Right leg
  • 16,080
  • 7
  • 48
  • 81
  • Thank you so much for this insight. Whilst still very confusing, I should be able to use this information as leverage for whenever I come across lambda's like the ones in my question. This answer, coupled with @sainoba, provide an informative lambda expression rule and how it evaluates variables of the likes. – juiceb0xk Sep 12 '17 at 14:26
1

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.

sainoba
  • 158
  • 1
  • 13
  • That's awesome, thanks for that insightful answer. Could I please ask why function2 only ever prints 4, and prints it out 4 times? By my understanding, it should be appending 0-4 in the list, but as you've pointed out, it's only appending 4's; how is this? – juiceb0xk Sep 12 '17 at 14:30
  • The functions2 list contains lambda functions that will print the value of **number** when they are called. Since the last value of number was 4, all functions will print 4. – sainoba Sep 12 '17 at 14:33