[Tkinter] messed callbacks

G

Giacomo Boffi

i have this test program (that i already posted on it.comp.lang.python)

----[ test.py ]
from Tkinter import *

def output(s):
print s

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)

root=Tk()
fr=Frame(root,relief='raised',borderwidth=5).pack(fill='both',expand=1)
doit(fr,['pippo','pluto','paperino','zio_paperone'])
Button(root,text='Q',command=root.destroy).pack(expand=1,fill='x')
root.mainloop()
----

when i execute it from the command line, click on the 4 buttons from
top to bottom and quit (you can do the same), this is what i get

aiuole: python test.py
gray->black
black->gray
gray->black
black->gray
aiuole:

as you see, the button 0 and 1 have callbacks different from my
expectations, as my script was intended to give

cyan->blue
blue->cyan
gray->black
black->gray

why the messed callbacks? what's the right thing to do?

tia,
g
 
D

Diez B. Roggisch

Giacomo said:
i have this test program (that i already posted on it.comp.lang.python)

----[ test.py ]
from Tkinter import *

def output(s):
print s

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)

root=Tk()
fr=Frame(root,relief='raised',borderwidth=5).pack(fill='both',expand=1)
doit(fr,['pippo','pluto','paperino','zio_paperone'])
Button(root,text='Q',command=root.destroy).pack(expand=1,fill='x')
root.mainloop()
----

when i execute it from the command line, click on the 4 buttons from
top to bottom and quit (you can do the same), this is what i get

aiuole: python test.py
gray->black
black->gray
gray->black
black->gray
aiuole:

as you see, the button 0 and 1 have callbacks different from my
expectations, as my script was intended to give

cyan->blue
blue->cyan
gray->black
black->gray

why the messed callbacks? what's the right thing to do?

Closures in python contain names, not the objects they refer to. So when you
rebind that name (as you do above in your loop), the created callbacks will
only refer to the last bound value of a name.

Create new closures, or bind arguments as defaults:

funcs = []

def create_func(i):
return lambda: i

for i in xrange(10):
funcs.append(lambda i=i: i)
funcs.append(create_func(i))

for f in funcs:
print f()

Diez
 
G

Giacomo Boffi

Diez B. Roggisch said:
Giacomo said:
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?

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?
the created callbacks will only refer to the last bound value of a
name.

Create new closures, or bind arguments as defaults:

funcs = []

def create_func(i):
return lambda: i

for i in xrange(10):
funcs.append(lambda i=i: i)
funcs.append(create_func(i))

for f in funcs:
print f()

i tried to understand, and maybe i have understood a thing or two...

funcs = []

def create_func(i):
return lambda: i

for i in xrange(10):
funcs.append(lambda i=i: i)
funcs.append(create_func(i))
funcs.append(lambda: i) # this is my addition

for f in funcs:
print f()

ok, i'll try again following your advice

thank you very much
g
 
G

Giacomo Boffi

Giacomo Boffi said:
ok, i'll try again following your advice

,----[ test.py ]
| from Tkinter import *
|
| def output(s):
| print s
|
| def create_cb(a,b):
| return lambda: output(a+'->'+b)
|
| 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=create_cb(c1,c2)).pack()
| Button(subframe,text='<',command=create_cb(c2,c1)).pack()
| subframe.pack(fill='x',expand=1)
|
| root=Tk()
| doit(root,['cyan','blue','gray','black'])
| Button(root,text='Q',command=root.destroy).pack(expand=1,fill='x')
| root.mainloop()
`----

works ok, now i have to fully understand my previous error

again, thanks you very much
g
 
T

Terry Reedy

Giacomo said:
Diez B. Roggisch said:
Giacomo said:
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.
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.

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.

Terry Jan Reedy
 
G

Giacomo Boffi

Scott David Daniels said:
Giacomo said:
| def create_cb(a,b):
| return lambda: output(a+'->'+b)
| | 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=create_cb(c1,c2)).pack()
| Button(subframe,text='<',command=create_cb(c2,c1)).pack()
| subframe.pack(fill='x',expand=1) ...
works ok, now i have to fully understand my previous error

This is really why functools.partial exists.

i take due note, tx
Also note from Pep 8, spaces are cheap and make the code easier to
read.

space-crunch was just for posting on usenet, 80 cols terminals etc.

grazie,
g
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,995
Messages
2,570,228
Members
46,818
Latest member
SapanaCarpetStudio

Latest Threads

Top