2

I'm learning a little bit of Tkinter in Python, creating interactive windows. The window I'm currently making is one that, given a dict of contacts and their respective contact info, creates a button for each contact which, when pressed, displays their contact info.

The two sample contacts I have are named 'Marvin' and 'Minsky', and their info is stored in a dict named book:

import Tkinter as tkinter

# 'Phonebook' with contact info for two people.
book = {"Marvin": {"Mobile": "1234567890", "Email": "marvin@gmail.com"}, 
"Minsky": {"Mobile": "9087865342", "Email": "minsky@yahoo.com"}} 

window = tkinter.Tk() # Make window object

def showinfo(name):
  # Displays info for person whose button was clicked

  # Displays info by configuring labels named 'mobile' and 'email', 
  # using values in 'book'
  mobile.configure(text = book[name]["Mobile"])
  email.configure(text = book[name]["Email"])

Here's the important part. Instead of the buttons simply using command = showinfo(name) to call showinfo(), as I first tried, a lambda expression is used:

for name in sorted(phonedict.book.keys()): 
  # Create button for each alphabetically sorted name
  btn = tkinter.Button(text = name, command = lambda arg = name: showinfo(arg))
  btn.pack()

And the rest of the code is just the labels that showinfo() modifies:

# Window section where contact info is displayed via labels
mobile_lbl = tkinter.Label(text = "Mobile:")
mobile_lbl.pack()
mobile = tkinter.Label(text = "")
mobile.pack()
email_lbl = tkinter.Label(text = "Email:")
email_lbl.pack()
email = tkinter.Label(text = "")
email.pack()

# Display window
window.mainloop()

When run, this program does exactly what I want it to do, modifying the labels correctly when each button is clicked.

If command = showinfo(name) is used in place of the lambda, however, NameError: global name 'mobile' is not defined is thrown because (I think) it tries to execute the button's command when the button is created instead of when it is pressed.

Why would the use of a lambda expression in the button's command keep the command from executing until the button is clicked? What functional purpose is it serving?

Zach W
  • 23
  • 1
  • 4

2 Answers2

4

Like you guessed, when you use command = showinfo(name) you are asking python to immediately call showinfo(name), and then assign the result of that to the command attribute.

The command attribute must be given a reference to a function. lambda is a convenient way to create an anonymous function and return the reference, which gets assigned to the attribute. Inside that anonymous function you can call any other function you want, and that inner code isn't executed until the anonymous function is executed.

The functional purpose of lambda is to create a temporary, unnamed function that can be passed to other functions or stored as an attribute. It is a convenient way (but not the only way1) to create a wrapper around a callback that requires an argument.

1Another way to accomplish the same thing is with functools.partial. Another method would be to write your own decorator.

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • +1 for the concise explaination of the lambda. Can you elaborate a little on the "but not the only way" comment in your answer? Just a curious noob trying to learn everything! Thanks! – Christopher Pearson Apr 22 '15 at 18:33
  • 1
    @ChristopherPearson: I've added a brief mention of functools.partial and decorators to my answer. – Bryan Oakley Apr 22 '15 at 20:18
2

A callback is simply a function that you pass around to other functions so those functions can call it. showinfo(name) is not a callback because the function is called immediately before the Button is constructed and the return value of showinfo seems to be None (if a function doesn't return anything it returns None by default in Python).

showinfo by itself, could be a callback because it's a function, however the problem is that it requires a positional argument. In Python, positional arguments are required:

def f1(callback): callback()
def f2(arg1): pass

f1(f2) # TypeError: f2() takes exactly 1 argument (0 given)

The way your code solves this is with a lambda expression that takes a default parameter that we define on-the-fly to be name. So what we are saying is, here is a function that by has a keyword argument arg that by default is set to name, when this function is called, call showinfo with that default argument:

btn = tkinter.Button(text = name, command=lambda arg=name: showinfo(arg))

But why do we need to use default arguments? Isn't that a complicated way of doing things? Yes, yes it is. You can simply do this instead:

btn = tkinter.Button(text = name, command=lambda: showinfo(name))

This is a lambda that takes no arguments and is a perfectly valid equivalent to what you're doing. This is because showinfo and name are part of the closure of the lambda you're creating, so they can be accessed by the lambda itself even after the lambda function has been passed to another function.

Yes, but it's necessary because each lambda function you pass in needs the value of name at the time that the lambda was created. Relying on a closure to keep the value of name won't do the trick, because the value of name changes on every iteration of the loop.

Shashank
  • 13,713
  • 5
  • 37
  • 63
  • 1
    Thanks! One problem I just found with the last suggestion of omitting `arg=name` and just having `showinfo(name)`, is that this will result in all the callbacks simply referencing the variable instead of attaching the *current value* of the variable to the callback for each loop iteration. The result is that without `arg=name`, every button created in the loop will just use the *current value* of `name` at the time the button is clicked. – Zach W Apr 22 '15 at 18:29
  • @ZachW Noted! I have made an edit to the answer that goes over this. Good call, pun intended. :) – Shashank Apr 22 '15 at 18:38