In the simple
button examples in the preceding section, the callback
handler was simply an existing function that killed the GUI program. It’s
not much more work to register callback handlers that do something a bit
more useful.
Example 7-12
defines a callback handler of its own in Python.
Example 7-12. PP4E\Gui\Intro\gui3.py
import sys
from tkinter import *
def quit(): # a custom callback handler
print('Hello, I must be going...') # kill windows and process
sys.exit()
widget = Button(None, text='Hello event world', command=quit)
widget.pack()
widget.mainloop()
The window created by this script is shown in
Figure 7-11
. This script and its GUI
are almost identical to the last example. But here, thecommand
option specifies a function we’ve
defined locally. When the button is pressed, tkinter calls thequit
function in this file to handle the event,
passing it zero arguments. Insidequit
,
theprint
call statement types a
message on the program’sstdout
stream,
and the GUI process exits as before.
Figure 7-11. A button that runs a Python function
As usual,stdout
is normally the
window that the program was started from unless it’s been redirected to a
file. It’s a pop-up DOS console if you run this program by clicking it on
Windows; add aninput
call beforesys.exit
if you have trouble seeing the
message before the pop up disappears. Here’s what the printed output looks
like back in
standard
stream world
when the button is pressed; it is generated by a Python function called
automatically by tkinter:
C:\...\PP4E\Gui\Intro>python gui3.py
Hello, I must be going...
C:\...\PP4E\Gui\Intro>
Normally, such messages would be displayed in the GUI, but we
haven’t gotten far enough to know how just yet. Callback functions usually
do more, of course (and may even pop up new independent windows
altogether), but this example illustrates the basics.
In general, callback handlers can be any callable object: functions,
anonymous functions generated with lambda expressions, bound methods of
class or type instances, or class instances that inherit a__call__
operator overload method. ForButton
press callbacks, callback handlers always
receive no arguments (other than an automaticself
, for bound methods); any state information
required by the callback handler must be provided in other ways—as global
variables, class instance attributes, extra arguments provided by an
indirection layer, and so on.
To make this a bit more concrete, let’s take a quick look at some
other ways to code the callback handler in this example.
Recall that the
Python lambda expression generates a new, unnamed function
object when run. If we need extra data passed in to the handler
function, we can register lambda expressions to defer the call to the
real handler function, and specify the extra data it needs.
Later in this part of the book, we’ll see how this can be more
useful, but to illustrate the basic idea,
Example 7-13
shows what
Example 7-12
looks like when
recoded to use a lambda instead of adef
.
Example 7-13. PP4E\Gui\Intro\gui3b.py
import sys
from tkinter import * # lambda generates a function
widget = Button(None, # but contains just an expression
text='Hello event world',
command=(lambda: print('Hello lambda world') or sys.exit()) )
widget.pack()
widget.mainloop()
This code is a bit tricky because lambdas can contain only an
expression; to emulate the original script, this version uses anor
operator to force two expressions
to be run (print
works as the first,
because it’s a function call in Python 3.X—we don’t need to resort to
usingsys.stdout
directly).
More typically, lambdas
are used to provide an indirection layer that passes along
extra data to a callback handler (I omitpack
andmainloop
calls in the following snippets for
simplicity):
def handler(A, B): # would normally be called with no args
...use A and B...
X = 42
Button(text='ni', command=(lambda: handler(X, 'spam'))) # lambda adds arguments
Although tkinter invokescommand
callbacks with no arguments, such a
lambda can be used to provide an indirect anonymous function that wraps
the real handler call and passes along information that existed when the
GUI was first constructed. The call to the real handler is, in effect,
deferred
, so we can add the extra arguments it
requires. Here, the value of global variableX
and string'spam'
will be passed to argumentsA
andB
,
even though tkinter itself runs callbacks with no arguments. The net
effect is that the lambda serves to map a no-argument function call to
one with arguments supplied by the lambda.
If lambda syntax confuses you, remember that a lambda expression
such as the one in the preceding code can usually be coded as a simpledef
statement instead, nested or
otherwise. In the following code, the second function does exactly the
same work as the prior lambda—by referencing it in the button creation
call, it effectively defers invocation of the actual callback handler so
that extra arguments can be passed:
def handler(A, B): # would normally be called with no args
...use A and B...
X = 42
def func(): # indirection layer to add arguments
handler(X, 'spam')
Button(text='ni', command=func)
To make the need for
deferrals more obvious, notice what happens if you code a
handler call in the button creation call itself without a lambda or
other intermediate
function—
the
callback runs immediately
when the button is
created
, not when it is later clicked. That’s why we need to
wrap the call in an intermediate function to defer its
invocation:
def handler(name):
print(name)
Button(command=handler('spam')) # BAD: runs the callback now!
Using either a lambda or a callable reference serves to defer
callback invocation until the event later occurs. For example, using a
lambda to pass extra data with an inline function definition that defers
the call:
def handler(name):
print(name)
Button(command=(lambda: handler('spam'))) # OK: wrap in a lambda to defer
is always equivalent to the longer, and to some observers less
convenient,
double
-
function
form:
def handler(name):
print(name)
def temp():
handler('spam')
Button(command=temp) # OK: refence but do not call
We need only the zero-argument lambda or the zero-argument
callable reference, though,
not both
—it makes no
sense to code a lambda which simply calls a function if no extra data
must be passed in and only adds an extra pointless call:
def handler(name):
print(name)
def temp():
handler('spam')
Button(command=(lambda: temp())) # BAD: this adds a pointless call!
As we’ll see later, this includes references to other callables
like bound methods and callable instances which retain state in
themselves—if they take zero arguments when called, we can simply name
them at widget construction time, and we don’t need to wrap them in a
superfluous
lambda.
Although the prior
section’s lambda and intermediate function techniques
defer calls and allow extra data to be passed in, they also raise some
scoping issues that may seem subtle at first glance. This is core
language territory, but it comes up often in practice in conjunction
with GUI.
For instance,
notice that thehandler
function in the prior section’s
initial code could also refer toX
directly, because it is a global variable (and would exist by the time
the code inside the handler is run). Because of that, we might make
the handler a one-argument function and pass in just the string'spam'
in the lambda:
def handler(A): # X is in my global scope, implicitly
...use global X and argument A...
X = 42
Button(text='ni',command=(lambda: handler('spam'))
)
For that matter,A
could be
moved out to the global scope too, to remove the need for lambda here
entirely; we could register the handler itself and cut out the
middleman.
Although simple in this trivial example, arguments are generally
preferred to globals, because they make external dependencies more
explicit, and so make code easier to understand and change. In fact,
the same handler might be usable in other contexts, if we don’t couple
it to global variables’ values. While you’ll have to take it on faith
until we step up to larger examples with more complex state retention
needs, avoiding globals in callbacks and GUIs in general both makes
them more reusable, and supports the notion of multiple instances in
the same program. It’s good programming practice, GUI or not.
More subtly, notice
that if the button in this example was constructed
inside a
function
rather than at the top level of
the file, nameX
would no longer be
global but would be in the enclosing function’s local scope; it seems
as if it would disappear after the function exits and before the
callback event occurs and runs the lambda’s code:
def handler(A, B):
...use A and B...
def makegui():
X = 42
Button(text='ni', command=(lambda: handler(X, 'spam'))) # remembers X
makegui()
mainloop() # makegui's scope is gone by this point
Luckily, Python’s enclosing scope reference model means that the
value ofX
in the local scope
enclosing the lambda function is automatically retained, for use later
when the button press occurs. This usually works as we want today, and
automatically handles variable references in this role.
To make such enclosing scope usage explicit, though, default
argument values can also be used to remember the values of variables
in the enclosing local scope, even after the enclosing function
returns. In the following code, for instance, the default argument
nameX
(on the left side of theX=X
default) will remember object42
, because the variable nameX
(on the right side of theX=X
) is evaluated in the enclosing
scope, and the generated function is later called without any
arguments:
def handler(A, B): # older Pythons: defaults save state
...use A and B...
def makegui():
X = 42
Button(text='ni',command=(lambda X=X: handler(X, 'spam'))
)
Since default arguments are evaluated and saved when the lambda
runs (not when the function it creates is later called), they are a
way to explicitly remember objects that must be accessed again later,
during event processing. Because tkinter calls the lambda function
later with no arguments, all its defaults are used.
This was not an issue in the original version of this example
because nameX
lived in the global
scope, and the code of the lambda will find it there when it is run.
When nested within a function, though,X
may have disappeared after the enclosing
function exits.
While they can make some external dependencies more explicit,
defaults are not usually required (since Python 2.2, at least) and are
not used for this role in best practice code today. Rather, lambdas
simply defer the call to the actual handler and provide extra handler
arguments. Variables from the enclosing scope used by the lambda are
automatically
retained, even after the enclosing
function exits.
The prior code listing, for example, can today normally be coded
as we did earlier—nameX
in the
handler will be automatically mapped toX
in the enclosing scope, and so effectively
remember whatX
was when the button
was made:
def makegui():
X = 42 # X is retained auto
Button(text='ni',command=(lambda: handler(X, 'spam'))
) # no need for defaults
We’ll see this technique put to more concrete use later. When
using classes to build your GUI, for instance, theself
argument is a local variable in
methods, and is thus automatically available in the bodies of lambda
functions. There is no need to pass it in explicitly with
defaults:
class Gui:
def handler(self, A, B):
...use self, A and B...
def makegui(self):
X = 42
Button(text='ni',command=(lambda: self.handler(X, 'spam'))
)
Gui().makegui()
mainloop()
When using classes, though, instance attributes can provide
extra state for use in callback handlers, and so provide an
alternative to extra call arguments. We’ll see how in a moment. First,
though, we need to take a quick non-GUI diversion into a dark corner
of Python’s scope rules to understand why default arguments are still
sometimes necessary to pass values into nested lambda functions,
especially in GUIs.
Although you may still see defaults used to pass in enclosing
scope references in some older Python code, automatic enclosing scope
references are generally preferred today. In fact, it seems as though
the newer nested scope lookup rules in Python automate and replace the
previously manual task of passing in enclosing scope values with
defaults altogether.
Well, almost. There is a catch. It turns out that within a
lambda (ordef
), references to
names in the enclosing scope are actually resolved when the generated
function is
called
, not when it is created.
Because of this, when the function is later called, such name
references will reflect the latest or final assignments made to the
names anywhere in the enclosing scope, which are not necessarily the
values they held when the function was made. This holds true even when
the callback function is nested only in a module’s global scope, not
in an enclosing function; in either case, all enclosing scope
references are resolved at function call time, not at function
creation time.
This is subtly different from default argument values, which are
evaluated once when the function is
created
, not
when it is later called. Because of that, defaults can still be useful
for remembering the values of enclosing scope variables as they were
when you made the function. Unlike enclosing scope name references,
defaults will not have a different value if the variable later changes
in the enclosing scope, between function creation and call. (In fact,
this is why mutable defaults like lists retain their state between
calls—they are created only once, when the function is made, and
attached to the function itself.)
This is normally a nonissue, because most enclosing scope
references name a variable that is assigned just once in the enclosing
scope (theself
argument in class
methods, for example). But this can lead to coding mistakes if not
understood, especially if you create functions within a
loop
; if those functions reference the loop
variable, it will evaluate to the value it was given on the
last
loop iteration in
all
the functions generated. By contrast, if you use defaults instead,
each function will remember the
current
value of
the loop variable, not the last.
Because of this difference, nested scope references are not
always sufficient to remember enclosing scope values, and defaults are
sometimes still required today. Let’s see what this means in terms of
code. Consider the following nested function (this section’s code
snippets are saved in file
defaults.py
in the examples package, if you
want to experiment with them).
def simple():
spam = 'ni'
def action():
print(spam) # name maps to enclosing function
return action
act = simple() # make and return nested function
act() # then call it: prints 'ni'
This is the simple case for enclosing scope references, and it
works the same way whether the nested function is generated with adef
or a lambda. But notice that
this still works if we assign the enclosing scope’sspam
variable
after
the
nested function is created:
def normal():
def action():
return spam # really, looked up when used
spam = 'ni'
return action
act = normal()
print(act()) # also prints 'ni'
As this implies, the enclosing scope name isn’t resolved when
the nested function is made—in fact, the name hasn’t even been
assigned yet in this example. The name is resolved when the nested
function is
called
. The same holds true for
lambdas:
def weird():
spam = 42
return (lambda: spam * 2) # remembers spam in enclosing scope
act = weird()
print(act()) # prints 84
So far, so good. Thespam
inside this nested lambda function remembers the value that this
variable had in the enclosing scope, even after the enclosing scope
exits. This pattern corresponds to a registered GUI callback handler
run later on events. But once again, the nested scope reference really
isn’t being resolved when the lambda is run to create the function;
it’s being resolved when the generated function is later
called
. To make that more apparent, look at this
code:
def weird():
tmp = (lambda: spam * 2) # remembers spam
spam = 42 # even though not set till here
return tmp
act = weird()
print(act()) # prints 84
Here again, the nested function refers to a variable that hasn’t
even been assigned yet when that function is made. Really, enclosing
scope references yield the latest setting made in the enclosing scope,
whenever the function is called. Watch what happens in the following
code:
def weird():
spam = 42
handler = (lambda: spam * 2) # func doesn't save 42 now
spam = 50
print(handler()) # prints 100: spam looked up now
spam = 60
print(handler()) # prints 120: spam looked up again now
weird()
Now, the reference tospam
inside the lambda is different each time the generated function is
called! In fact, it refers to what the variable was set to
last
in the enclosing scope at the time the
nested function is called, because it is resolved at function call
time, not at function creation time.
In terms of GUIs, this becomes significant most often when you
generate callback handlers within loops and try to use enclosing scope
references to remember extra data created within the loops. If you’re
going to make functions within a loop, you have to apply the last
example’s behavior to the loop variable:
def odd():
funcs = []
for c in 'abcdefg':
funcs.append((lambda: c)) # c will be looked up later
return funcs # does not remember current c
for func in odd():
print(func(), end=' ') # OOPS: print 7 g's, not a,b,c,... !
Here, thefunc
list simulates
registered GUI callback handlers associated with widgets. This doesn’t
work the way most people expect it to. The variablec
within the nested function will always beg
here, the value that the variable
was set to on the final iteration of the loop in the enclosing scope.
The net effect is that all seven generated lambda functions wind up
with the same extra state information when they are later
called.
Analogous GUI code that adds information to lambda callback
handlers will have similar problems—all buttons created in a loop, for
instance, may wind up doing the same thing when clicked! To make this
work, we still have to pass values into the nested function with
defaults in order to save the current value of the loop variable (not
its future value):
def odd():
funcs = []
for c in 'abcdefg':
funcs.append((lambda c=c: c)) # force to remember c now
return funcs # defaults eval now
for func in odd():
print(func(), end=' ') # OK: now prints a,b,c,...
This works now only because the default, unlike an external
scope reference, is evaluated at function
creation
time, not at function call time. It
remembers the value that a name in the enclosing scope had when the
function was made, not the last assignment made to that name anywhere
in the enclosing scope. The same is true even if the function’s
enclosing scope is a module, not another function; if we don’t use the
default argument in the following code, the loop variable will resolve
to the same value in all seven functions:
funcs = [] # enclosing scope is module
for c in 'abcdefg': # force to remember c now
funcs.append((lambda c=c: c)) # else prints 7 g's again
for func in funcs:
print(func(), end=' ') # OK: prints a,b,c,...
The moral of this story is that enclosing scope name references
are a replacement for passing values in with defaults,
but
only
as long as the name in the enclosing scope will not
change to a value you don’t expect after the nested function is
created. You cannot generally reference enclosing scope loop variables
within a nested function, for example, because they will change as the
loop progresses. In most other cases, though, enclosing scope
variables will take on only one value in their scope and so can be
used freely.
We’ll see this phenomenon at work in later examples that
construct larger GUIs. For now, remember that enclosing scopes are not
a complete replacement for defaults; defaults are still required in
some contexts to pass values into callback functions. Also keep in
mind that classes are often a better and simpler way to retain extra
state for use in callback handlers than are nested functions. Because
state is explicit in classes, these scope issues do not apply.
The next two sections cover this in
detail.