0

I'm creating a Menu in Python 3.7 using Tkinter. When I create a menu with hard-coded values like:

obj_menu = Menu(textbox, tearoff=0)
obj_menu.add_command(label="a", command=lambda : foo("a", index))
index = index + 1
obj_menu.add_command(label="b", command=lambda : foo("b", index))

everything works fine. in my callback method foo() I get a and b correct values when select 1st and 2nd menu item respectively.

But when I create this menu in for loop, I always get last char in my callback method foo().

for idx, char in enumerate(alphabets):
    obj_menu.add_command(label=char, command=lambda : foo(char, idx))

I'm confused as why the method only gets value of last index no matter which menu index I select. Am I missing something?

Hussain Mansoor
  • 2,934
  • 2
  • 27
  • 40
  • You can still use `lambda` by forcing a closure. Read [Tkinter assign button command in loop with lambda](https://stackoverflow.com/questions/17677649/tkinter-assign-button-command-in-loop-with-lambda) – Henry Yik Jul 15 '19 at 06:53
  • Thanks @HenryYik that was the solution. – Hussain Mansoor Jul 15 '19 at 07:20

3 Answers3

1

If you did want to create a list of functions, rather than using lambda you could make use of Python's partial() function from the functools library. For example:

for idx, char in enumerate(alphabets):
    obj_menu.add_command(label=char, command=partial(foo, char, idx))

The following shows the results of the two different approaches in a small test example:

from functools import partial

def foo(char, idx):
    print(char, idx)
    return idx

alphabets = "abc"

print("Using lambda")
funcs = []

for idx, char in enumerate(alphabets):
    funcs.append([char, lambda : foo(char, idx)])

for char, func in funcs:
    print(char, func())

print("\nUsing partial")
funcs = []

for idx, char in enumerate(alphabets):
    funcs.append([char, partial(foo, char, idx)])

for char, func in funcs:
    print(char, func())

This would display the following output:

Using lambda
a 2
a 2
b 2
b 2
c 2
c 2

Using partial
a 0
a 0
b 1
b 1
c 2
c 2

As can be seen, the first approach fails, resulting in the last lambda variant being used for all calls, whereas the partial() functions works correctly.

Martin Evans
  • 45,791
  • 17
  • 81
  • 97
0

When you create a lambda expression of that form, you are actually doing equivalent of something like this:

index = [whatever value]
def _implicit_func():
    return foo("a", index)
obj_menu.add_command(label="a", command=_implicit_func)

I hope that you can see that this index variable is also available outside of the function's scope. This means that you can edit this variable, and it will alter all future executions of said function.

If you are still confused, here's a reference on Python variables' scopes.

Piotr Kamoda
  • 956
  • 1
  • 9
  • 24
0

Solution was to create temporary variable in lambda and use them in method call.

for idx, char in enumerate(alphabets):
obj_menu.add_command(label=char, command=lambda c=char, i=idx : foo(c, i))

Thanks to this answer: https://stackoverflow.com/a/17677768/970422

Hussain Mansoor
  • 2,934
  • 2
  • 27
  • 40