3

How do I get the modify_attr() function (below) not to capture/update the value of b in the following for loop? (simplified version, occurring within a mainloop()):

for b in range(x):

    button = tk.Button(button_frame, text="<", command=lambda: current_instance.modify_attr(b, -1))
    button.place(x=110, y=80 + 18 * b)
    button = tk.Button(button_frame, text=">", command=lambda: current_instance.modify_attr(b, 1))
    button.place(x=120, y=80 + 18 * b)

The goal is to generate two columns of buttons and to bind each button pair to a (somewhat complicated) pair of functions (conditional reduce_by_one / increase_by_one functions).

  • x = the number of button pairs I need generated
  • current_instance = a class instance
  • modify_attr = a class method accepting two arguments (well I guess three if we include self)

My understanding (based on this and other things I've read over the past few) is that this issue is a common one. At its heart, the problem is that all values of b with respect to modify_attr() wind up equal to len(x) (rather than the value of b at the moment I intend to bind that command to the button). The result is a series of buttons that are positioned properly (via the b value(s) in button.place) but all pointing to the last element in the list they're supposed to be modifying.

I previously encountered this exact problem and was able to work around it using a helper function. But for some reason I am unable to apply that solution here (again, simplified for clarity):

for b in range(len(the_list)):
    def helper_lambda(c):
        return lambda event: refresh_frame(the_list[c])
    window.bind(b + 1, helper_lambda(b))

Does that make sense? Exact same issue, helper_lamdba works like a charm. Now, in this case, I'm binding a hotkey rather than a button command, but I simply can't get my head around why it would work differently. Because fundamentally the problem is with the for loop, not the functions within. But when I implement a helper function in my button loop it fails like a not-a-charm.

Here is my failed attempt to apply that helper strategy:

for b in range(x):
    def helper_lambda(c, modifier):
        return lambda event: current_instance.modify_attr(c, modifier)

    button = tk.Button(button_frame, text="<", command=lambda: helper_lambda(b, -1))
    button.place(x=110, y=80 + 18 * b)
    button = tk.Button(button_frame, text=">", command=lambda: helper_lambda(b, 1))
    button.place(x=120, y=80 + 18 * b)

What am I doing wrong? Also, why does it behave this way? Is anyone using incrementor values outside of for loops?!

Nathan
  • 118
  • 6

2 Answers2

2

The second approach may work with few changes:

for b in range(x):
    def helper_lambda(c, modifier):
        return lambda: current_instance.modify_attr(c, modifier)  # removed event argument

    button = tk.Button(button_frame, text="<", command=helper_lambda(b, -1))
    button.place(x=110, y=80 + 18 * b)
    button = tk.Button(button_frame, text=">", command=helper_lambda(b, 1))
    button.place(x=150, y=80 + 18 * b)

However, you can use lambda directly without the helper function:

for b in range(x):
    button = tk.Button(button_frame, text="<", command=lambda b=b: current_instance.modify_attr(b, -1))
    button.place(x=110, y=80 + 18 * b)
    button = tk.Button(button_frame, text=">", command=lambda b=b: current_instance.modify_attr(b, 1))
    button.place(x=150, y=80 + 18 * b)
acw1668
  • 40,144
  • 5
  • 22
  • 34
  • 1
    It just assigns a *default* value (`b` from the for loop) to the argument `b`. Default value is bound when creating the lambda. – acw1668 Jan 19 '21 at 03:43
1

This is a case where functools.partial is a better option than a lambda expression.

from functools import partial

for b in range(x):

    button = tk.Button(button_frame, text="<", command=partial(current_instance.modify_attr, b, -1))
    button.place(x=110, y=80 + 18 * b)
    button = tk.Button(button_frame, text=">", command=partial(current_instance.modify_attr, b, 1))
    button.place(x=120, y=80 + 18 * b)

partial receives the value of b as an argument, rather than simply capturing the name b for later use.

chepner
  • 497,756
  • 71
  • 530
  • 681