使用Python中的WeakSet实现回调功能

14

我正在研究如何在Python中实现简单的回调功能。我认为我可以使用weakref.WeakSet来实现,但显然我还有些遗漏或误解。你可以在代码中看到,我首先尝试使用'ClassA'对象中回调方法的列表,但意识到这将使已添加到回调列表的对象保持活动状态。相反,我尝试使用weakref.WeakSet,但这也没有起作用(至少不是这种方式)。代码最后四行的注释解释了我想要发生的事情。

有人能帮助我吗?

from weakref import WeakSet
class ClassA:
    def __init__(self):
        #self.destroyCallback=[]
        self.destroyCallback=WeakSet()
    def __del__(self):
        print('ClassA object %d is being destroyed' %id(self))
        for f in self.destroyCallback:
            f(self)
class ClassB:
    def destroyedObjectListener(self,obj):
        print('ClassB object %d is called because obj %d is being destroyed'%(id(self),id(obj)))
a1=ClassA()
a2=ClassA()
b=ClassB()

a1.destroyCallback.add(b.destroyedObjectListener)
#a1.destroyCallback.append(b.destroyedObjectListener)
print('destroyCallback len() of obj: %d is: %d'%(id(a1),len(a1.destroyCallback))) # should be 1

a2.destroyCallback.add(b.destroyedObjectListener)
#a2.destroyCallback.append(b.destroyedObjectListener)
print('destroyCallback len() of obj: %d is: %d'%(id(a2),len(a2.destroyCallback))) # should be 1

del a1 # Should call b.destroyedObjectListener(self) in its __del__ method

del b # should result in no strong refs to b so a2's WeakSet should automatically remove added item

print('destroyCallback len() of obj: %d is: %d'%(id(a2),len(a2.destroyCallback))) # should be 0
del a2 # Should call __del__ method

更新:基于被接受的答案的解决方案可以在github上找到:git@github.com:thgis/PythonEvent.git

2个回答

22

您不能创建方法对象的弱引用。方法对象是短暂的;它们在访问实例上的名称时即时创建。请参阅描述符指南了解其工作原理。

当您访问方法名称时,会为您创建一个新的方法对象,当您将该方法添加到 WeakSet中时,不再存在对它的其他引用,因此垃圾回收会快乐地清理它。

您需要存储一些更持久的东西。存储实例对象本身将起作用,然后调用已注册回调函数上的预定义方法:

def __del__(self):
    for f in self.destroyCallback:
        f.destroyedObjectListener(self)

并且进行注册:

a1.destroyCallback.add(b)

你也可以通过给b添加__call__方法,使其成为可调用的:

class ClassB:
    def __call__(self,obj):
        print('ClassB object %d is called because obj %d '
              'is being destroyed' % (id(self), id(obj)))

另一种方法是存储对底层函数对象的引用以及对实例的引用:

import weakref


class ClassA:
    def __init__(self):
        self._callbacks = []
    
    def registerCallback(self, callback):
        try:
            # methods
            callback_ref = weakref.ref(callback.__func__), weakref.ref(callback.__self__)
        except AttributeError:
            callback_ref = weakref.ref(callback), None
        self._callbacks.append(callback_ref)

    def __del__(self):
        for callback_ref in self._callbacks:
            callback, arg = callback_ref[0](), callback_ref[1]
            if arg is not None:
                # method
                arg = arg()
                if arg is None:
                    # instance is gone
                    continue
                callback(arg, self)
                continue
            else:
                if callback is None:
                    # callback has been deleted already
                    continue
                callback(self)

演示:

>>> class ClassB:
...     def listener(self, deleted):
...         print('ClassA {} was deleted, notified ClassB {}'.format(id(deleted), id(self)))
... 
>>> def listener1(deleted):
...     print('ClassA {} was deleted, notified listener1'.format(id(deleted)))
... 
>>> def listener2(deleted):
...     print('ClassA {} was deleted, notified listener2'.format(id(deleted)))
... 
>>> # setup, one ClassA and 4 listeners (2 methods, 2 functions)
... 
>>> a = ClassA()
>>> b1 = ClassB()
>>> b2 = ClassB()
>>> a.registerCallback(b1.listener)
>>> a.registerCallback(b2.listener)
>>> a.registerCallback(listener1)
>>> a.registerCallback(listener2)
>>> 
>>> # deletion, we delete one instance of ClassB, and one function
... 
>>> del b1
>>> del listener1
>>> 
>>> # Deleting the ClassA instance will only notify the listeners still remaining
... 
>>> del a
ClassA 4435440336 was deleted, notified ClassB 4435541648
ClassA 4435440336 was deleted, notified listener2

1
我现在已经收到了你的建议,看起来非常有效。我将解决方案封装在一个事件类中,并仅实现了add()、remove()和signal()以进行最小化使用。也许它会扩展。如果有人感兴趣,可以通过以下方式从github下载该解决方案:git@github.com/thgis/PythonEvent.git - thomas
太棒了!我只建议做一个小改进,使用getattr(callback, '__self__', None)代替异常。 - Shital Shah
@ShitalShah:不行,因为那样你会创建一个对“None”的弱引用。我们想要存储“None”本身,而不是对它的弱引用。这样你就可以区分方法和函数了。 - Martijn Pieters

1
尝试以下更改:

更新WeakSet:

a1.destroyCallback.add(b)

所以WeakSet持有对b的引用。
然后在ClassA的__del__方法中,像这样触发回调:
for f in self.destroyCallback:
        f.destroyedObjectListener(self)

谢谢您的回复,Glenn。 我想这样做可能会起作用...我认为我只能创建一个对象的弱引用,而不是我尝试的对象方法。您的方法将创建对b的弱引用,因此可以工作,但是必须显式编写函数名称“destroyedObjectListener”,事件驱动的想法消失了。但是,无论如何,您让我得出结论,问题在于我尝试创建对类方法的弱引用。 - thomas

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