So I've got this simple demo for my problem:
from functools import partial
import tkinter as tk
root = tk.Tk()
root.title("Test")
root.geometry("400x400")
colors = ["red", "green", "blue"]
for x in range(3):
firstFrame = tk.Frame(root, bg=colors[x])
firstFrame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
firstFrame.bind("<Enter>", lambda e: print(f"Enter: {x}"))
firstFrame.bind("<Leave>", lambda e: partial(print, f"Leave: {x}")())
root.mainloop()
When I run the code and hover over the red section, then the green one and finally over the blue one before closing the window, I get this output:
Enter: 2
Leave: 2
Enter: 2
Leave: 2
Enter: 2
Leave: 2
What I need is:
Enter: 0
Leave: 0
Enter: 1
Leave: 1
Enter: 2
Leave: 2
As you can see I've tried multiple approaches to get the desired output, like using partial
to create a whole new function instead of simply calling print
, but nothing yielded any results.
CodePudding user response:
This is a common problem in Python lambdas. A for
loop variable only exists once, so if a lambda is created inside of a for
loop, then the lambda will capture the changing, mutable value of the variable.
The trick to get around this is to exploit Python's default arguments. When a default argument is created for a function, it effectively creates a new property on that function object storing the current value of the variable. This is a separate variable, so future reassignments of the loop variable will not affect the default argument's value.
firstFrame.bind("<Enter>", lambda e, arg=x: print(f"Enter: {arg}"))
You'll see people name the default argument the same thing as the enclosing variable. The following snippet is exactly equivalent to the former; the x
argument to the lambda shadows the x
argument from the for
loop.
firstFrame.bind("<Enter>", lambda e, x=x: print(f"Enter: {x}"))
This just reduces the cognitive load for the programmer to switch between two variable names for what is, conceptually, the same variable.