连接到 pyqtSignal 的 lambda 函数中对象的生命周期

6
假设我有一个对象,并且希望在发出PyQt信号时执行其中的一个方法。并且假设我希望它执行一个不由信号传递的参数。因此,我创建了一个lambda作为信号的槽函数:
class MyClass(object):
    def __init__(self, model):
        model.model_changed_signal.connect(lambda: self.set_x(model.x(), silent=True))

通常情况下,使用PyQt的信号和槽时,信号连接不会阻止垃圾回收。当连接的槽对象被垃圾回收时,相应的信号被发射时该槽将不再被调用。
然而,当使用lambda表达式时,它是如何工作的呢?我没有存储lambda表达式的引用,但信号槽连接仍然可以正常工作。因此,lambda表达式没有被垃圾回收。
如果现在将MyClass的实例设置为None,那么该实例也不会被垃圾回收:发射model_changed_signal仍然会成功执行lambda表达式。因此,MyClass实例的引用被保留在某个地方(可能是lambda表达式的上下文中?)- 而我并不想要这样。
为什么会发生这种情况呢?
1个回答

7
你的示例中的 lambda 形成了一个闭包。也就是说,它是一个嵌套函数,引用了其封闭作用域中可用的对象。每个创建闭包的函数都会为需要维护引用的每个项保留一个cell 对象
在你的示例中,lambda 创建了一个闭包,其中引用了 __init__ 方法作用域内的本地变量 selfmodel。如果你在某个地方保留了对 lambda 的引用,则可以通过其 __closure__ 属性检查其闭包的所有 cell 对象。在你的示例中,它将显示类似于以下内容:
>>> print(func.__closure__)
(<cell at 0x7f99c16c5138: MyModel object at 0x7f99bbbf0948>, <cell at 0x7f99c16c5168: MyClass object at 0x7f99bbb81390>)

如果您删除此处显示的所有其他对MyModelMyClass对象的引用,单元格保留的对象仍将保留。因此,在进行对象清理时,您应始终明确断开与可能形成闭包的相关对象连接的所有信号。
请注意,在进行信号/槽连接时,PyQt以不同方式处理包装的C++槽和Python实例方法。当它们连接到信号时,这些类型的可调用对象的引用计数不会增加,而lambda、定义的函数、partial对象和静态方法则会增加。这意味着如果删除了对后者类型的可调用对象的所有其他引用,则任何剩余的信号连接都将使其保持活动状态。如果需要,断开信号将允许这样的连接可调用对象被垃圾回收。
上述情况的唯一例外是类方法。在创建到这些方法的连接时,PyQt会创建一个特殊的包装器,因此,如果删除了所有对它们的其他引用,并发出了信号,则会引发异常,如下所示:
TypeError: 'managedbuffer' object is not callable

以上内容适用于PyQt5以及大多数版本的PyQt4(4.3及以上版本)。

这证实并澄清了lambda对MyClass和MyModel的引用,谢谢!我仍然不清楚的是为什么lambda的垃圾回收与其他函数对象不同。比如说,如果我实例化一个类并将其方法连接如下:model.model_changed_signal.connect(ModelListener().handle_signal),那么ModelListener的实例将被垃圾回收,handle_signal将永远不会被调用。 - tjalling
@tjalling。垃圾回收对于lambda来说完全相同。区别完全在于闭包,任何函数都可以形成闭包。是闭包保留了额外的引用,而不是函数。在您评论中给出的示例中没有闭包。 - ekhumoro
但是难道不需要引用闭包吗?如果我丢弃了对lambda的最后一个引用(例如将其设置为“None”),那么lambda和相应的闭包将被垃圾回收,对吗?为什么将lambda连接到“pyqtSignal”会防止该lambda(和闭包)被垃圾回收,而将绑定函数连接到信号不会防止对象(绑定函数所绑定的对象)被垃圾回收呢? - tjalling
抱歉拼写错误,编辑窗口关闭后才注意到。 - tjalling
@tjalling,我已经更新了我的答案,并添加了相关信息。 - ekhumoro
显示剩余5条评论

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