[英]Python lambda function is not being called correctly from within a for loop
I am trying to make a calculator using Tkinter in Python.我正在尝试在 Python 中使用 Tkinter 制作计算器。 I use a for loop to draw the buttons in, and I'm trying to use lambda functions so the buttons' actions are only called when pressed instead of calling right when the program starts.我使用 for 循环来绘制按钮,并且我正在尝试使用 lambda 函数,因此按钮的操作仅在按下时调用,而不是在程序启动时调用。 When I attempt this, however, a "list index out of range" error is thrown.但是,当我尝试这样做时,会引发“列表索引超出范围”错误。
This is the relevant section which draws the buttons and sets their processes:这是绘制按钮并设置其流程的相关部分:
# button layout
# ( ) AC **
# 7 8 9 /
# 4 5 6 *
# 1 2 3 -
# 0 . = +
# list that holds the buttons' file names in order
imgOrder = ["lpr", "rpr", "clr", "pow",
"7", "8", "9", "div",
"4", "5", "6", "mul",
"1", "2", "3", "sub",
"0", "dot", "eql", "add"];
# list containing the actual values assigned to buttons in order
charOrder = ["(", ")", "AC", "**",
"7", "8", "9", "/",
"4", "5", "6", "*",
"1", "2", "3", "-",
"0", ".", "=", "+"];
imgIndex = 0;
for rowi in range(1, 6):
for coli in range(4):
# fetch and store the img
img = PhotoImage(file="images/{}.gif".format(imgOrder[imgIndex]));
# create button
button = Button(self, bg="white", image=img,
borderwidth=0, highlightthickness=0, activebackground="white",
command=lambda: self.process(charOrder[imgIndex]));
# set button image
button.image = img;
# put the button in its proper row and column
button.grid(row=rowi, column=coli, sticky=N+S+E+W);
imgIndex += 1;
# pack the GUI
self.pack(fill=BOTH, expand=1);
# processes button presses
def process(self, button):
print("Button {button} pressed!");
# AC clears the display
if (button == "AC"):
# clear
self.display["text"] = "";
# otherwise, the number/operator can be tacked on the end
else:
self.display["text"] += button;
Specifically the line that is throwing the error is the line with the lambda function, but I don't know of any way to only make the buttons register on click, short of writing out this block for every button individually.具体来说,引发错误的行是 lambda function 的行,但我不知道有什么方法可以只让按钮在点击时注册,没有为每个按钮单独写出这个块。
This is a classic case of unwanted closure.这是一个不受欢迎的关闭的经典案例。 Here's a simplified example:这是一个简化的示例:
funcs = []
for i in range(3):
funcs.append(lambda: i + 1)
for func in funcs:
print(func())
One might expect that this will print 1 2 3
, but it prints 3 3 3
instead.人们可能期望这将打印1 2 3
,但它会打印3 3 3
。 The reason is, lambda
is a closure , closing over the variable i
(capturing it in its context).原因是, lambda
是一个闭包,关闭变量i
(在其上下文中捕获它)。 When we execute the functions, the variable i
is left at its last value in the loop ( 2
).当我们执行这些函数时,变量i
保留在循环中的最后一个值( 2
)。 To reiterate, the lambda
does not capture the value , but the variable .重申一下, lambda
不捕获值,而是捕获变量。 To avoid this, we want to pass the current value of i
into the function as a parameter .为了避免这种情况,我们希望将i
的当前值作为参数传递给 function。 To do that, we can construct a function that accepts i
as the parameter, captures the parameter into the closure and returns the "customised" function we want:为此,我们可以构造一个接受i
作为参数的 function,将参数捕获到闭包中并返回我们想要的“定制” function:
from functools import partial
funcs = []
for i in range(3):
funcs.append((lambda val: lambda: val + 1)(i))
for func in funcs:
print(func())
Equivalently, we can use functools.partial
, which does just this:等效地,我们可以使用functools.partial
,它就是这样做的:
from functools import partial
funcs = []
for i in range(3):
funcs.append(partial(lambda val: val + 1, i))
for func in funcs:
print(func())
Here, lambda val: val + 1
will expect a parameter;在这里, lambda val: val + 1
将期望一个参数; partial(lambda val: val + 1, 0)
will produce a new function where the first parameter is fixed at 0
- basically, lambda: 0 + 1
. partial(lambda val: val + 1, 0)
将产生一个新的 function ,其中第一个参数固定为0
- 基本上, lambda: 0 + 1
。 This captures no variables, and thus avoids the problem you encountered.这不捕获任何变量,从而避免了您遇到的问题。
tl;dr: tl;博士:
command=partial(lambda i: self.process(charOrder[i]), imgIndex)
The issue is that the variable imgIndex
at the end of the double loop is 20. The lambda expression binds self.process(charOrder[imgIndex])
to the variable , no to its value, so at the exit of the loop it will evaluate to self.process(charOrder[20])
.问题是双循环结束时的变量imgIndex
为 20。 lambda 表达式将self.process(charOrder[imgIndex])
绑定到变量,而不绑定到它的值,因此在循环退出时它将评估为self.process(charOrder[20])
。
A way around is to use lambda imgIndex=imgIndex: self.process(charOrder[imgIndex])
, which will pass the parameter by value.一种解决方法是使用lambda imgIndex=imgIndex: self.process(charOrder[imgIndex])
,它将按值传递参数。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.