[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