【问题标题】:Differences between lambda and partial when connecting clicked signal in PyQt/PySide在 PyQt/PySide 中连接点击信号时 lambda 和 partial 的区别
【发布时间】:2021-05-27 13:57:58
【问题描述】:

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

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

【问题讨论】:

  • @musicamante 如果您不确定您的参考是否回答了这个问题,为什么要针对这个问题发起Close。回到你的参考,它总结了 lambda 作为实例方法,而 partial 作为静态方法,它没有解释代码的输出。
  • 这是 StackOverflow 在投票结束后立即创建的自动评论,您可能不同意或者我可能错了,只需回答“不,它没有”。

标签: python lambda pyqt5 signals-slots partial


【解决方案1】:

在讨论 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, f'button{n}', 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))

【讨论】:

    【解决方案2】:

    据我了解,原因是每次“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_())
    

    【讨论】:

      【解决方案3】:

      你的答案在这里: 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), self,
                                                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_())
      

      【讨论】:

      • 1 您的代码在我的环境中不起作用(Window10 64,python 3.7.9),单击任一按钮时会输出button 1; 2 我已经找到了解决方法,就是使用functools.partial,但我也想知道为什么lambda不起作用。
      • 查看链接中的答案
      • 奇怪的是它可以在 PySide2 上工作,但不能在 PyQt5 上工作。
      • 我检查了您的参考答案和其他一些答案,例如 stackoverflow.com/questions/47941743/… 。似乎变量的名称空间是原因。我对Python闭包不熟悉,需要参考更多资料。
      • @foool 此答案中的代码无法正常工作的原因是由默认信号参数的不同(但密切相关)问题引起的。见这里:stackoverflow.com/a/40310832/984421.
      猜你喜欢
      • 2021-02-15
      • 2013-07-14
      • 2013-10-22
      • 1970-01-01
      • 1970-01-01
      • 2012-06-08
      • 2013-12-31
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多