Почему WeakSet теряет ссылку на метод сразу после добавления
Самостоятельно изучаю Python, и в качестве упражнения придумал написать простую систему событий, которая позволяет подписываться на события и публиковать их. Для хранения обработчиков использую WeakSet
, чтобы не реализовывать метод unsubscribe
явно. Однако столкнулся с неожиданным поведением: объекты обработчиков исчезают из WeakSet
сразу после добавления, если только я не ставлю отладчик на паузу.
Вот упрощённый код:
from typing import Dict, Type, Callable, TypeVar
from weakref import WeakSet
T = TypeVar('T')
class Event:
pass
class EventBus:
def __init__(self) -> None:
self._handlers: Dict[Type[Event], WeakSet[Callable]] = {}
def subscribe(self, event_type: type[T], handler: Callable[[T], None]) -> None:
if event_type not in self._handlers:
self._handlers[event_type] = WeakSet()
self._handlers[event_type].add(handler)
# Здесь handler ещё виден в WeakSet!
def publish(self, event: Event) -> None:
event_type = type(event)
for t in event_type.mro():
if t in self._handlers:
for handler in self._handlers[t]:
handler(event)
# Тест (упрощённый)
class HandlerRecorder:
def __init__(self):
self._called_events = []
def record(self, event):
self._called_events.append(event)
class CustomEvent(Event):
pass
def test_subscribe_and_publish_single_event():
bus = EventBus()
recorder = HandlerRecorder()
bus.subscribe(CustomEvent, recorder.record) # <-- подписка
event = CustomEvent() # <-- после этой строки handler исчезает!
bus.publish(event)
assert len(recorder._called_events) == 1 # Падает: 0 != 1
Что я наблюдаю в отладчике: после добавления обработчика в методе subscribe
он появляется в WeakSet
. Но как только я перехожу к следующей строке (event = CustomEvent()
), обработчик исчезает из WeakSet
. Но если я подержу отладчик подольше на паузе, проблема не возникает.
Вопросы: почему WeakSet
сразу забывает добавленный обработчик? Если WeakSet
здесь не подходит, как лучше организовать хранение обработчиков, чтобы избежать утечек памяти при удалении объектов-подписчиков?
Ответы (1 шт):
Проблема в том, что recorder.record
в вашем коде - это временный объект, так называемый bound method. Поскольку этот метод нигде кроме WeakSet не сохраняется - он и пропадает из него при первой же возможности.
Чтобы нормально хранить подобные обработчики, надо использовать WeakMethod:
class WeakHandlerList:
def __init__(self):
self._handlers = []
def subscribe(self, handler):
wref = weakref.WeakMethod(handler, self._handlers.remove)
self._handlers.append(wref)
def unsubscribe(self, handler):
wref = weakref.WeakMethod(handler)
self._handlers.remove(wref)
def publish(self, *args, **kwargs):
for wref in self._handlers:
wref()(*args, **kwargs)