在PyQt4中循环连接插槽和信号

13

我正在尝试使用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”。 为什么会这样,并且我该如何修复?

3个回答

20

这就是Python中作用域、名称查找和闭包的定义方式。

Python只通过赋值和函数参数列表引入命名空间中的新绑定。因此,i实际上并没有在lambda的命名空间中定义,而是在__init__()的命名空间中定义。因此,在lambda中对i进行名称查找最终会在__init__()的命名空间中结束,其中i最终被绑定到9。这就是所谓的“闭包”。

您可以通过将i作为具有默认值的关键字参数传递来解决这些不太直观(但定义良好)的语义问题。正如前面所说,参数列表中的名称会在本地命名空间中引入新的绑定,因此lambda中的i.__init__()中的i相互独立:

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))

我在这里使用新式信号和槽语法只是为了方便起见,旧式语法同样适用。

谢谢。我会选择functools.partial的解决方案。 - lukad

4
你正在创建闭包。闭包实际上捕获了一个变量,而不是变量的值。在__init__结束时,irange(0,10)的最后一个元素,即9。在此作用域中创建的所有lambda表达式都引用这个i,只有在它们被调用时才会获取i的值(然而,单独调用__init__的不同调用会创建引用不同变量的闭包)。
有两种常见的避免方法:
1. 使用默认参数:lambda i=i: self._number(i)。这样做是因为默认参数在函数定义时绑定了一个值。 2. 定义一个辅助函数helper = lambda i: (lambda: self._number(i))并在循环中使用helper(i)。这样做是因为“外部”i在绑定i时被评估,并且 - 如前所述 - 在下一个helper调用中创建的下一个闭包将引用不同的变量。

0

1
请不要使用QSignalMapper。它是C++ 98的遗留物,该版本没有lambda或partial函数,因此这种解决方法是必要的恶。然而,在Python中,QSignalMapper相对于functools.partial()或lambda来说过于臃肿且多余。使用partial()lambda的解决方案比QSignalMapper更短小精悍、更优雅。 - user355252
哦,这关乎选择,我会选择 QSignalMapper 以确保与 Qt/C++ 兼容。 - ismail
1
当然这是关于选择,但我就是不理解你的选择,也无法跟随它。我使用Python来开发Qt应用程序,正因为它 不是 C++,如果我放弃了Python所有的特殊功能来维护或达到C++兼容性,我还不如直接使用C++ ;) - user355252

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接