我尝试使用 PyQt4 构建一个计算器,但从按钮连接“clicked()”信号无法按预期工作。 我为 for 循环内的数字创建按钮,然后尝试连接它们。
def __init__(self):
for i in range(0,10):
self._numberButtons += [QPushButton(str(i), self)]
self.connect(self._numberButtons[i], SIGNAL('clicked()'), lambda : self._number(i))
def _number(self, x):
print(x)
当我点击按钮时,所有按钮都会打印出“9”。 为什么会这样?我该如何解决这个问题?
这就是 Python 中如何定义范围、名称查找和闭包。
Python 仅通过赋值和函数参数列表在命名空间中引入新的绑定。因此,
i
实际上并不是在lambda
的命名空间中定义的,而是在__init__()
的命名空间中定义的。因此,在 lambda 中对 i
的名称查找最终会在 __init__()
的命名空间中结束,其中 i
最终绑定到 9
。这就是所谓的“关闭”。
您可以通过将
i
作为具有默认值的关键字参数传递来解决这些确实不直观(但定义明确)的语义。如前所述,参数列表中的名称在本地命名空间中引入了新的绑定,因此 i
中的 lambda
然后变得独立于 i
中的 .__init__()
:
self._numberButtons[i].clicked.connect(lambda checked, i=i: self._number(i))
更新:
clicked
有一个默认的checked
参数,它将覆盖i
的值,因此它必须添加到关键字值之前的参数列表中。
一个更具可读性、不那么神奇的替代方案是
functools.partial
:
self._numberButtons[i].clicked.connect(partial(self._number, i))
我在这里使用新型信号和槽语法只是为了方便,旧式语法的工作原理是一样的。
你正在创造封闭。闭包真正捕获的是变量,而不是变量的值。在
__init__
的末尾,i
是range(0, 10)
的最后一个元素,即9
。您在此范围内创建的所有 lambda 都引用此 i
,并且仅当调用它们时,它们才会在调用时获取 i
的值(但是,单独调用 __init__
创建引用单独的 lambda变量!)。
有两种流行的方法可以避免这种情况:
lambda i=i: self._number(i)
。这是因为默认参数在函数定义时绑定了一个值。helper = lambda i: (lambda: self._number(i))
并在循环中使用 helper(i)
。这是有效的,因为“外部”i
是在绑定i
时评估的,并且 - 如前所述 - 在下一次调用 helper
中创建的下一个闭包将引用不同的变量。