Python装饰器注册回调时类方法self上下文丢失问题求解
根本原因
核心问题是两种注册方式拿到的回调对象本质完全不同,叠加装饰器执行时机早于实例创建,最终导致上下文错位:
- 直接在
__init__中传入self.stop时,传入的是绑定方法对象:Python会自动把当前Game实例绑定到方法的第一个self参数上,后续回调触发时,Event传入的两个参数(Event实例、pygame事件对象)会自动填充到方法的剩余形参位置,参数匹配完全正常。 - 用
@EventPull.dregister装饰类中定义的stop方法时,装饰器的执行时机是Game类定义阶段,此时没有任何Game类的实例被创建,装饰器拿到的callback是类命名空间下的未绑定原始函数,没有关联任何Game实例。当事件触发时,Event类按callback(self, event)的形式调用回调,会把Event自身实例作为第一个位置参数传给原始函数,刚好占了原本留给Game实例的self形参位,直接导致self指向错误、参数错位。
修复方案
根据使用场景选择对应方案即可:
方案1:保留实例初始化阶段注册的写法
对于需要访问实例属性(比如代码中要修改self.running)的实例方法,最稳妥的方式就是在__init__中传入绑定后的方法注册,不需要强行用装饰器:
class Game: def __init__(self) -> None: self.running = True EventPull.register(pygame.QUIT, self.stop) def stop(self, event_obj, pygame_event): print('call', self, event_obj, pygame_event) self.running = False
这种写法没有隐式逻辑,执行流程完全可控,不会出现上下文丢失问题。
方案2:拆分装饰器的标记和注册逻辑,实现装饰器式注册
如果一定要用装饰器简化代码,可以让装饰器只做「事件类型标记」,不直接在类定义阶段注册未绑定函数,等实例初始化时,再把当前实例的绑定方法统一注册到事件池:
CallbackType = callable class EventPull: _events: dict[int, Event] = dict() _MARK_ATTR = "_bind_event_types" @classmethod def register(cls, event_type: int, *callbacks: CallbackType): event = cls._events.setdefault(event_type, Event(event_type)) event.add(*callbacks) @classmethod def dregister(cls, event_type: int): def decorator(callback): # 仅给函数打事件标记,不做实际注册 if not hasattr(callback, cls._MARK_ATTR): setattr(callback, cls._MARK_ATTR, set()) getattr(callback, cls._MARK_ATTR).add(event_type) return callback return decorator @classmethod def bind_instance_events(cls, instance): """注册实例上所有打了事件标记的绑定方法""" for attr_name in dir(instance): method = getattr(instance, attr_name) if not callable(method): continue event_types = getattr(method, cls._MARK_ATTR, None) if not event_types: continue for etype in event_types: cls.register(etype, method) # 原有Event类逻辑无需修改 class Event: type: int _callbacks: set[CallbackType] def __init__(self, type: int, *callbacks: CallbackType) -> None: self.type = type self._callbacks = set(callbacks) def emit(self, event: pygame.event.Event): for callback in self._callbacks: callback(self, event) def add(self, *callbacks: CallbackType): self._callbacks.update(callbacks)
使用时只需要在实例初始化时调用一次绑定方法即可:
class Game: def __init__(self) -> None: self.running = True # 注册当前实例所有标记过的事件回调 EventPull.bind_instance_events(self) @EventPull.dregister(pygame.QUIT) def stop(self, event_obj, pygame_event): print('call', self, event_obj, pygame_event) self.running = False
这种方式既保留了装饰器的写法简洁性,也不会丢失实例上下文。
补充说明:如果回调是不需要访问实例状态的静态方法、类方法,直接用原有装饰器注册不会有问题,因为这两类方法不依赖实例绑定。
内容的提问来源于stack exchange,提问作者Eugene
相关产品推荐
相关产品推荐

