Tkinter Puzzler

T

Tim Daneliuk

I am trying to initialize a menu in the following manner:

for entry in [("Up", KeyUpDir), ("Back", KeyBackDir), ("Home", KeyHomeDir), ("Startdir", KeyStartDir), ("Root",
KeyRootDir)]:

func = entry[1]
UI.ShortBtn.menu.add_command(label=entry[0], command=lambda: func(None))

However, at runtime, each of the menu options binds to the *last* function
named in the list (KeyStartDir).

Explicitly loading each entry on its own line works fine:

UI........command=lambda:KeyWHATEVERDir(None)

Any ideas why the first form does not fly?


TIA,
 
F

F. Petitjean

Le 07 Jan 2005 05:28:31 EST, Tim Daneliuk a écrit :
I am trying to initialize a menu in the following manner:

for entry in [("Up", KeyUpDir), ("Back", KeyBackDir), ("Home", KeyHomeDir), ("Startdir", KeyStartDir), ("Root",
KeyRootDir)]:

func = entry[1]
UI.ShortBtn.menu.add_command(label=entry[0], command=lambda: func(None))
The problem is that you *call* the callback : the command parameter is
bound to the result of func(None)
However, at runtime, each of the menu options binds to the *last* function
named in the list (KeyStartDir).

Explicitly loading each entry on its own line works fine:

UI........command=lambda:KeyWHATEVERDir(None)

Any ideas why the first form does not fly?
I would simplify the code like ;

add_cmd = UI.ShortBtn.menu.add_command
for label, func in (("Up", KeyUpDir), .... ):
add_cmd(label=label, command=func)

And have
def KeyUpDir(arg=None):
# whatever
 
E

Eric Brunel

Tim said:
I am trying to initialize a menu in the following manner:

for entry in [("Up", KeyUpDir), ("Back", KeyBackDir), ("Home",
KeyHomeDir), ("Startdir", KeyStartDir), ("Root", KeyRootDir)]:

func = entry[1]
UI.ShortBtn.menu.add_command(label=entry[0], command=lambda:
func(None))

However, at runtime, each of the menu options binds to the *last* function
named in the list (KeyStartDir).

Explicitly loading each entry on its own line works fine:

UI........command=lambda:KeyWHATEVERDir(None)

Any ideas why the first form does not fly?

This has nothing to do with Tkinter, but only with the way nested scopes work:
to put it roughly, your "lambda: func(None)" only knows about the *name* func,
which is not actually evaluated until the button is pressed. And when the button
is pressed, the name func is bound to the last command in the loop.

To do what you want, change your code to:

for entry in (...):
UI.ShortBtn.menu.add_command(
label=entry[0],
command=lambda func=entry[1]: func(None)
)

This way, the value for the func parameter is evaluated when the function is
defined and not when it is called.

HTH
 
A

Alex Martelli

Tim Daneliuk said:
I am trying to initialize a menu in the following manner:

for entry in [("Up", KeyUpDir), ("Back", KeyBackDir), ("Home",
KeyHomeDir), ("Startdir", KeyStartDir), ("Root", KeyRootDir)]:

func = entry[1]
UI.ShortBtn.menu.add_command(label=entry[0], command=lambda: func(None))

However, at runtime, each of the menu options binds to the *last* function
named in the list (KeyStartDir).

Explicitly loading each entry on its own line works fine:

UI........command=lambda:KeyWHATEVERDir(None)

Any ideas why the first form does not fly?

One word: late binding. Well, two, pedantically speaking;-).

The lambda you're passing as the value for 'command' is a closure: it
knows it will have to look up name 'func' in the environment in which
it's embedded -- but also that it's meant to do that lookup as late as
possible, each time it's called.

If you wanted to do the lookup just once, at the time lambda executes
and created an anonymous function rather than each time said anonymous
function is called, you could have expressed that...:
command=lambda func=func: func(None)
Here, func is a local variable (argument) of the anonymous function, and
its "default value" is set ONCE, when the anon function is created.

Back to your code, when your anon function is called, it looks up name
'func' in the surrounding environment... and there it finds it bound to
whatever it was RE-bound to the LAST time...


Point to remember: a closure looks up free-variable names in its
surrounding environment *as late as possible*, i.e., when the function
object is called; while default argument values are evaluated *at
function creation time* (when lambda or def executes, not when the
resulting function object gets called).


Alex
 
T

Tim Daneliuk

Tim said:
I am trying to initialize a menu in the following manner:

for entry in [("Up", KeyUpDir), ("Back", KeyBackDir), ("Home",
KeyHomeDir), ("Startdir", KeyStartDir), ("Root", KeyRootDir)]:

func = entry[1]
UI.ShortBtn.menu.add_command(label=entry[0], command=lambda:
func(None))

However, at runtime, each of the menu options binds to the *last* function
named in the list (KeyStartDir).

Explicitly loading each entry on its own line works fine:

UI........command=lambda:KeyWHATEVERDir(None)

Any ideas why the first form does not fly?


TIA,

Thanks All - great responses!
 

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

Forum statistics

Threads
473,744
Messages
2,569,484
Members
44,905
Latest member
Kristy_Poole

Latest Threads

Top