Почему 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 шт):

Автор решения: Pavel Mayorov

Проблема в том, что 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)
→ Ссылка