在PyQt/PySide中连接点击信号时lambda和partial之间的区别

2 人关注

当把一组按钮的几个点击信号连接到一个带参数的槽函数时,我遇到了一个信号槽的问题。

替换代码0】和 functools.partial 可以用在以下方面。

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

而在某些情况下,它们的表现是不同的。 下面的代码显示了一个例子,它期望在点击每个按钮的时候打印出它的文本。 但是当使用lambda方法时,输出结果总是 "button 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_())

Personly, I agree that ButtonGroup method from the accepted answer is the right&elegant solution to this type of issue.

这里有一些关于这个问题的参考资料。

循环中的lambda

用Qt信号传输额外数据

2 个评论
@musicamante 如果你不确定你的参考资料是否回答了这个问题,为什么要对这个问题发起一个 Close 。回到你的参考资料,其中结论是 lambda 是实例方法,而 partial 是静态方法,它没有解释代码的输出。
这是StackOverflow在接近投票时自动创建的评论,你可能不同意,也可能是我错了,只要回答 "不,没有"。
python
lambda
pyqt5
signals-slots
partial
foool
foool
发布于 2021-02-25
3 个回答
ekhumoro
ekhumoro
发布于 2021-02-25
已采纳
0 人赞同

在讨论lambda/partial之间的区别和其他一些问题之前,我将简单介绍一下你的代码例子的一些实际修复方法。

有两个主要问题。第一个是在for-loop中绑定名字的常见问题,在这个问题中讨论过。 循环中的Lambda .第二个是PyQt特有的默认信号参数的问题,在这个问题中讨论过。 无法从Python循环中创建的Button发送信号 .鉴于此,你的例子可以通过这样连接信号来解决。

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

所以这就把n的当前值缓存在一个默认参数中,同时还添加了一个checked参数,以阻止n被信号发射的参数覆盖。

这个例子也可以从重新写一个更成文的方式中受益,因为它消除了讨厌的评价黑客。

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, f'button{n}', button)
self.buttonGroup.buttonClicked[int].connect(self.printtext)

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

至于lambda和partial的区别问题:主要是前者隐含地创建一个closure在本地变量上,而后者则明确地在内部存储传递给它的参数。

在python中,闭包常见的 "问题 "是,定义在循环内的函数不会捕捉到所包围变量的当前值。所以当函数被调用后,它只能访问最后看到的值。有一个最近在python-ideas邮件列表中讨论了改变这种行为的问题。,但似乎没有得出任何确定的结论。不过,未来的某个版本的python还是有可能去除这个小疣子。这个问题被partial函数完全避免了,因为它创建了一个可调用对象它将传递给它的参数作为只读属性来存储。因此,在lambda中使用默认参数来显式存储变量的工作方法,实际上也是在使用这种方法。

这里还有一点值得介绍:PyQt和PySide在连接到带有默认参数的信号时的差异。似乎PySide把所有的槽都当作是用一个槽位装饰者而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 output:

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

PySide2 output:

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))
    
Parsa Hz
Parsa Hz
发布于 2021-02-25
0 人赞同

根据我的理解,原因是每次 "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_())
    
Parsa Hz
Parsa Hz
发布于 2021-02-25
0 人赞同

你的答案就在这里。 循环连接PyQt4中的槽和信号

This will work:

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), self,
                                          clicked=lambda n=n: print('button {}'.format(n+1)))
            layout.addWidget(new_b)
if __name__ == '__main__':
    import sys