微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

在 PyQt/PySide 中连接点击信号时 lambda 和部分之间的差异

如何解决在 PyQt/PySide 中连接点击信号时 lambda 和部分之间的差异

在将一组按钮的多个点击信号连接到带参数的单个槽函数时,我遇到了信号槽问题。

lambdafunctools.partial 可以如下使用:

user = "user"
button.clicked.connect(lambda: calluser(name))

from functools import partial
user = "user"
button.clicked.connect(partial(calluser,name))

虽然在某些情况下,它们的表现不同。 以下代码显示一个示例,该示例希望在单击时打印每个按钮的文本。 但使用 lambda 方法时,输出始终为“按钮 3”。 partial 方法按预期工作。

我怎样才能找到它们的不同点?

from PyQt5 import QtWidgets

class Program(QtWidgets.QWidget):
    def __init__(self):
        super(Program,self).__init__()
        self.button_1 = QtWidgets.QPushButton('button 1',self)
        self.button_2 = QtWidgets.QPushButton('button 2',self)
        self.button_3 = QtWidgets.QPushButton('button 3',self)
        from functools import partial
        for n in range(3):
            bh = eval("self.button_{}".format(n+1)) 
            # lambda method : always print `button 3`
            # bh.clicked.connect(lambda: self.printtext(n+1))
            bh.clicked.connect(partial(self.printtext,n+1))
        
        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.button_1)
        layout.addWidget(self.button_2)
        layout.addWidget(self.button_3)
         
    def printtext(self,n):
        print("button {}".format(n));

if __name__ == '__main__':

    import sys
    app = QtWidgets.QApplication(sys.argv)
    window = Program()
    window.show()
    sys.exit(app.exec_())

附言

就我个人而言,我同意接受答案中的 ButtonGroup 方法解决此类问题的正确且优雅的方法

这里是关于这个问题的一些参考:

lambda in a loop

Transmitting extra data with Qt Signals

解决方法

据我所知,原因是每次“partial”都会根据带有预定义参数的“printtext”函数创建一个新函数,因此您为每个按钮传递一个不同的函数。 而在 lambda 函数中,您的参数是对变量的引用,因此当您单击按钮时,循环已经运行并且该变量等于循环中的最后一个数字并打印 3。 原来你可以不偏不倚地做同样的事情(对我有用):

from PyQt5 import QtWidgets


class Program(QtWidgets.QWidget):

    @staticmethod
    def create_func(n):
        return lambda: print('button {}'.format(n+1))

    def __init__(self):
        super(Program,self).__init__()
        layout = QtWidgets.QVBoxLayout(self)
        for n in range(3):
            layout.addWidget(QtWidgets.QPushButton('button {}'.format(n+1),self,clicked=self.create_func(n)))


if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    window = Program()
    window.show()
    sys.exit(app.exec_())
,

在讨论 lambda/partial 和其他一些问题之间的区别之前,我将简要介绍您的代码示例的一些实用修复。

有两个主要问题。第一个是 for 循环中名称绑定的常见问题,如以下问题所述:Lambda in a loop。第二个是带有默认信号参数的 PyQt 特定问题,如本问题所述:Unable to send signal from Button created in Python loop。鉴于此,您的示例可以通过连接这样的信号来修复:

bh.clicked.connect(lambda checked,n=n: self.printtext(n+1))

因此,这会将 n 的当前值缓存在默认参数中,并添加一个 checked 参数以阻止 n 被信号发出的参数覆盖。

该示例还可以从以更惯用的方式重写以消除讨厌的 eval hack 中受益:

layout = QtWidgets.QVBoxLayout(self)
for n in range(1,4):
    button = QtWidgets.QPushButton(f'button {n}')
    button.clicked.connect(lambda checked,n=n: self.printtext(n))
    layout.addWidget(button)
    setattr(self,f'button{n}',button)

或使用 QButtonGroup:

self.buttonGroup = QtWidgets.QButtonGroup(self)
layout = QtWidgets.QVBoxLayout(self)
for n in range(1,4):
    button = QtWidgets.QPushButton(f'button {n}')
    layout.addWidget(button)
    self.buttonGroup.addButton(button,n)
    setattr(self,button)
self.buttonGroup.buttonClicked[int].connect(self.printtext)

(请注意,也可以在 Qt Designer 中通过选择按钮然后在上下文菜单中选择“分配给按钮组”来创建按钮组)。


关于 lambda 和 partial 区别的问题:主要是前者在局部变量上隐式地创建了一个 closure,而后者显式地在内部存储了传递给它的参数。

python 中闭包的常见“问题”是在循环内定义的函数不捕获封闭变量的当前值。所以当函数稍后被调用时,它只能访问最后看到的值。有一个discussion about changing this behaviour fairly recently on the python-ideas mailing list,但似乎没有得出任何确定的结论。尽管如此,python 的某些未来版本可能会删除这个小疣。 partial 函数完全避免了这个问题,因为它创建了一个 callable object,将传递给它的参数存储为只读属性。因此,在 lambda 中使用默认参数显式存储变量的解决方法是有效地使用相同的方法。

这里还有一点值得一提:PyQt 和 PySide 在连接到带有默认参数的信号时的区别。似乎 PySide 将所有插槽视为用 slot decorator 装饰;而 PyQt 以不同的方式对待未修饰的插槽。以下是差异的说明:

def __init__(self):
    ...
    self.button_1.clicked.connect(self.slot1)
    self.button_2.clicked.connect(self.slot2)
    self.button_3.clicked.connect(self.slot3)

def slot1(self,n=1): print(f'slot1: {n=!r}')
def slot2(self,*args,n=1): print(f'slot2: {args=!r},{n=!r}')
def slot3(self,x,n=1): print(f'slot1: {x=!r},{n!r}')

依次点击每个按钮后,产生如下输出:

PyQt5 输出:

slot1: n=False
slot2: args=(False,),n=1
slot3: x=False,n=1

PySide2 输出:

slot1: n=False
slot2: args=(),n=1
TypeError: slot3() missing 1 required positional argument: 'x'

如果插槽用 @QtCore.pyqtSlot() 修饰,则 PyQt5 的输出与上面显示的 PySide2 输出匹配。因此,如果您需要一个适用于 PyQt 和 PySide 的 lambda(或任何其他未修饰的插槽)的解决方案,您应该使用这个:

bh.clicked.connect(lambda *args,n=n: self.printtext(n))
,

你的答案在这里: Connecting slots and signals in PyQt4 in a loop

这会起作用:

from PySide2 import QtWidgets


class Program(QtWidgets.QWidget):

    def __init__(self):
        super(Program,self).__init__()
        layout = QtWidgets.QVBoxLayout(self)
        for n in range(3):
            new_b = QtWidgets.QPushButton('button {}'.format(n+1),clicked=lambda n=n: print('button {}'.format(n+1)))
            layout.addWidget(new_b)


if __name__ == '__main__':
    import sys

    app = QtWidgets.QApplication(sys.argv)
    window = Program()
    window.show()
    sys.exit(app.exec_())

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。