Используя Python WeakSet для включения функции обратного вызова

Я изучаю, могу ли я реализовать простую функцию обратного вызова в python. Я думал, что смогу использовать для этого слабыйref.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 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

UPDATE: решение, основанное на принятом ответе, можно найти в github: git @github.com: thgis/PythonEvent.git

Ответ 1

Вы не можете создавать слабые ссылки на объекты метода. Объекты метода недолговечны; они создаются "на лету", когда вы получаете доступ к имени в экземпляре. См. дескриптор howto, как это работает.

Когда вы обращаетесь к имени метода, для вас создается новый объект метода, и когда вы добавляете этот метод в 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

Ответ 2

Попробуйте выполнить следующие изменения:

Чтобы обновить WeakSet:

a1.destroyCallback.add(b)

поэтому WeakSet содержит ссылку на b.

Затем в методе __del__ класса A активируйте обратный вызов следующим образом:

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