[Tkinter] messed callbacks
Terry Reedy
tjreedy at udel.edu
Wed Sep 9 11:44:50 EDT 2009
Giacomo Boffi wrote:
> "Diez B. Roggisch" <deets at nospam.web.de> writes:
>
>> Giacomo Boffi wrote:
>>
>>> def doit(fr,lst):
>>> for c1,c2 in zip(lst[::2], lst[1::2]):
>>> subframe=Frame(fr)
>>> Label(subframe,text=c1+' <->
>>> '+c2).pack(side='left',expand=1,fill='both')
>>> Button(subframe,text='>',command=lambda: output(c1+'->'+c2)).pack()
>>> Button(subframe,text='<',command=lambda: output(c2+'->'+c1)).pack()
>>> subframe.pack(fill='x',expand=1)
>>>
>>> why the messed callbacks? what's the right thing to do?
Reedy's Lambda Rule: if you have a problem with code that uses lambda
expressions, rewrite with equivalent def statements and then review.
Untested revision:
def doit(fr,lst):
for c1,c2 in zip(lst[::2], lst[1::2]):
subframe=Frame(fr)
Label(subframe,text=c1+' <->'+c2)
.pack(side='left',expand=1,fill='both')
def cb12(): return output(c1+'->'+c2)
def cb21(): return output(c2+'->'+c1)
Button(subframe,text='>',command=cb12).pack()
Button(subframe,text='<',command=cb21).pack()
subframe.pack(fill='x',expand=1)
For most people, it somehow seems more obvious with the def form that
only the nonlocal names are captured, not the objects. In other words,
the function objects created in each iteration are duplicates of each
other. Since they are duplicates, one will do as well. The above should
work even if you move the def statements out of the loop and put them
*before* the for statement:
(again, untested)
def doit(fr,lst):
def cb12(): return output(c1+'->'+c2)
def cb21(): return output(c2+'->'+c1)
for c1,c2 in zip(lst[::2], lst[1::2]):
subframe=Frame(fr)
Label(subframe,text=c1+' <->'+c2)
.pack(side='left',expand=1,fill='both')
Button(subframe,text='>',command=cb12).pack()
Button(subframe,text='<',command=cb21).pack()
subframe.pack(fill='x',expand=1)
Now it should be *really* obvious that the def statements only capture
names: there *are no objects* to be captured when they are compiled!
A simpler example.
def f():
def g(): return i
for i in 1,2,3: pass
print(g())
f()
# prints 3!
The reason this can work is because the interpreter scans a function
code block twice: first to find the names, second to generate code based
on the findings of the first pass. So when it compiles "def g(): return
i", it has already looked ahead to discover that 'i' is local to f and
not a module global name.
>> Closures in python contain names, not the objects they refer to. So
>> when you rebind that name (as you do above in your loop),
>
> sorry, i'm not conscient of rebinding a name... what do you mean by
> "rebind that name" exactly?
Each iteration of the for loop rebinds the doit local names c1, c2 to a
new pair of values from the zip. When the loop finishes, they are bound
to the last pair of objects and these are the ones used when the
callbacks are called.
>> the created callbacks will only refer to the last bound value of a
>> name.
Capturing different objects within a function object for each iteration,
which you want, requires additional code that creates a new and
*different* (not duplicate) function object for each iteration.
>> Create new closures, or bind arguments as defaults:
>>
>> funcs = []
>>
>> def create_func(i):
>> return lambda: i
Or, as Scott D. D. pointed out, use functools.partial.
>>
>> for i in xrange(10):
>> funcs.append(lambda i=i: i)
>> funcs.append(create_func(i))
>>
>> for f in funcs:
>> print f()
Terry Jan Reedy
More information about the Python-list
mailing list