
本文介绍一种通过自定义元类(ABCMeta 子类)在抽象基类 Evaluator 的所有具体子类中自动、透明地注入异步日志装饰器的方法,解决装饰器在 @abstractmethod 上失效、同步/异步混用导致装饰器不执行等典型问题。
本文介绍一种通过自定义元类(abcmeta 子类)在抽象基类 `evaluator` 的所有具体子类中**自动、透明地注入异步日志装饰器**的方法,解决装饰器在 `@abstractmethod` 上失效、同步/异步混用导致装饰器不执行等典型问题。
在 Python 中,直接将装饰器(如 @log_results)叠加在 @abstractmethod 之上(例如 @log_results @abstractmethod def __call__(...))是无效的——因为 @abstractmethod 本身返回一个特殊的 __isabstractmethod__ 标记对象,并非可调用函数,导致外层装饰器实际作用于该标记而非方法体,因此 wrapper 永远不会被触发(仅打印 "this is called",而 "this is called too!" 不会输出)。
更深层的问题在于:__call__ 在抽象类中被声明为抽象方法,其具体实现由子类提供;而装饰逻辑必须作用于子类实际定义的 __call__ 方法上,且该方法是 async def,因此装饰器内部也必须是 async def wrapper(...) 并使用 await func(...) 调用,否则将引发 RuntimeWarning: coroutine '...' was never awaited 或运行时错误。
✅ 正确解法:元类驱动的自动装饰(Meta-Driven Auto-Decoration)
我们定义一个继承自 ABCMeta 的元类 EvaluatorMeta,在子类创建完成时(即 __init__ 阶段),检查该类是否为 Evaluator 的具体子类(排除 Evaluator 自身和 ABC),然后动态地将 log_results 装饰器应用到其 __call__ 方法上:
from functools import wraps
from abc import ABC, ABCMeta
import asyncio
def log_results(func):
"""异步日志装饰器:自动记录 evaluation_result 和配置"""
@wraps(func)
async def wrapper(self, *args, **kwargs):
# ✅ 关键:使用 await 调用原协程方法
result = await func(self, *args, **kwargs)
# ✅ 确保 self.logger 已初始化(由 Evaluator.__init__ 保证)
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 not getattr(cls, '__isabstractmethod__', False):
if hasattr(cls, '__call__') and not getattr(cls.__call__, '__isabstractmethod__', False):
# 动态装饰子类已实现的 __call__
original_call = getattr(cls, '__call__')
decorated_call = log_results(original_call)
setattr(cls, '__call__', decorated_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 __call__
# 抽象声明保留,实现交由子类,元类负责自动装饰
@abstractmethod
def __call__(self, *args, **kwargs):
pass子类 ScoreEvaluator 保持简洁,无需任何装饰器或手动日志调用:
class ScoreEvaluator(Evaluator):
# ... __init__, _create_metrics, load_evaluation_data 等保持不变 ...
async def __call__(self, *args, **kwargs): # ✅ 具体实现,无装饰器
# ... 所有业务逻辑(异步数据加载、预测、评估)...
return evaluation_results # ✅ 返回结果,日志由元类自动注入的 wrapper 记录⚠️ 注意事项:
-
元类时机关键:
EvaluatorMeta.__init__在类定义结束、实例化前执行,确保装饰发生在子类方法绑定之后; -
避免重复装饰:检查
not getattr(cls.__call__, '__isabstractmethod__', False)可防止对未实现的抽象__call__尝试装饰; -
异步一致性:装饰器内必须
await func(...),且func必须是协程函数(async def),否则会破坏异步链; -
属性可用性:
self.logger和self.framework_config在Evaluator.__init__中初始化,子类构造时已继承,wrapper中可安全访问; -
工厂兼容性:
EvaluatorFactory无需修改,evaluator()调用将自动触发装饰后的__call__。
✅ 总结:通过元类实现“零侵入式”横切日志,既强制统一了日志行为,又完全解耦了业务逻辑与基础设施关注点。这是在抽象基类体系中安全、可靠地应用装饰逻辑的推荐模式。

















