
本文介绍一种基于元类(Metaclass)的可靠方案,使自定义装饰器(如异步日志记录)能自动、透明地应用于所有继承自抽象基类 Evaluator 的具体子类的 __call__ 方法,避免手动装饰遗漏和同步/异步混用导致的装饰器失效问题。
本文介绍一种基于元类(metaclass)的可靠方案,使自定义装饰器(如异步日志记录)能自动、透明地应用于所有继承自抽象基类 `evaluator` 的具体子类的 `__call__` 方法,避免手动装饰遗漏和同步/异步混用导致的装饰器失效问题。
在 Python 中,直接将装饰器(如 @log_results)叠加在 @abstractmethod 之上(例如 @log_results @abstractmethod def __call__(...))是无效的:抽象方法本身不提供实现,装饰器无法绑定到实际执行逻辑上;而子类重写 __call__ 时若未显式应用该装饰器,则日志逻辑完全被跳过——这正是原问题中 "this is called too!" 从未打印的根本原因。
更关键的是,原装饰器 log_results 定义为同步函数,但子类 __call__ 方法是 async def,导致 func(*args, **kwargs) 实际返回一个协程对象而非执行结果,进而引发静默失败或类型错误。因此,装饰器必须与被装饰方法保持一致的调用协议(同步/异步)。
✅ 正确解法:元类驱动的自动装饰
我们通过自定义元类 EvaluatorMeta,在子类创建完成时(__init__ 阶段),自动为其 __call__ 方法应用适配的异步装饰器:
from functools import wraps
from abc import ABC, ABCMeta
def log_results(func):
"""异步装饰器:自动记录 evaluation_result 到 logger"""
@wraps(func)
async def wrapper(self, *args, **kwargs):
result = await func(self, *args, **kwargs) # ✅ await 原始协程
self.logger.log(evaluation_result=result, config=self.framework_config)
return result
return wrapper
class EvaluatorMeta(ABCMeta):
def __init__(cls, name, bases, dct):
super().__init__(name, bases, dct)
# 仅对直接继承自 Evaluator(非 ABC 本身)的子类生效
if Evaluator in bases and cls is not Evaluator:
if hasattr(cls, '__call__') and not getattr(cls.__call__, '__isabstractmethod__', False):
# 安全包装:仅当 __call__ 是已实现的普通方法(非 abstractmethod)
setattr(cls, '__call__', log_results(cls.__call__))
class Evaluator(ABC, metaclass=EvaluatorMeta):
def __init__(self, evaluation_config, framework_config, **kwargs):
self.evaluation_config = evaluation_config
self.framework_config = framework_config
self.logger = WandbLogger() # ✅ 初始化在父类,子类可直接使用
@abstractmethod
def _create_metrics(self) -> List[Metric]:
pass
@abstractmethod
def load_evaluation_data(self, data_path):
pass
# ✅ 移除原错误的 @log_results @abstractmethod 组合
@abstractmethod
def __call__(self, *args, **kwargs):
pass
? 关键设计说明
-
元类时机精准:
EvaluatorMeta.__init__在每个子类定义结束时触发,此时cls.__call__已是子类中定义的具体async def方法,可安全装饰。 -
严格作用域控制:
if Evaluator in bases and cls is not Evaluator确保只装饰子类,不干扰抽象基类自身。 -
异步兼容性:装饰器内部使用
await func(...),完美匹配async def __call__。 -
属性可访问性:
self.logger和self.framework_config在父类__init__中初始化,子类实例自然继承,装饰器中可直接引用。 -
无需子类干预:
ScoreEvaluator等子类保持原样,无需添加任何装饰器或修改__call__签名,日志逻辑全自动注入。
⚠️ 注意事项
- 若子类
__call__方法被意外声明为@staticmethod或@classmethod,需调整装饰器逻辑(当前方案假设为标准实例方法)。 - 元类仅影响类创建阶段,运行时动态替换
__call__不受此机制覆盖。 - 日志器(如
WandbLogger)需确保其log方法支持异步调用,否则需在wrapper中用await asyncio.to_thread(...)包装。
通过该方案,所有 Evaluator 子类在调用 await evaluator(...) 时,均会自动执行日志记录,真正实现“强制统一横切逻辑”,兼顾可维护性与健壮性。










