如何获取跨文件抽象类的所有子类,并实现可扩展的评估器与指标架构
嘿,看起来你已经摸到门道了!咱们先解决子类加载的问题,再聊聊如何设计一个能轻松扩展的评估器-指标架构,让团队新增功能时不用动核心代码。
一、解决跨文件子类动态加载的问题
你已经发现了,__subclasses__()返回空列表是因为子类还没被加载到内存里——Python只会在导入模块时才会执行模块里的类定义。这里给你两种更优雅的方案:
方案1:自动导入evaluators目录下的所有子类
手动在__init__.py里加新子类太麻烦,我们可以用pkgutil自动遍历目录导入所有模块:
修改evaluators/__init__.py:
import pkgutil import sys # 遍历当前目录下的所有非抽象类模块 for loader, module_name, is_pkg in pkgutil.walk_packages(__path__, __name__ + '.'): # 跳过抽象类所在的evaluator模块 if not is_pkg and module_name != f"{__name__}.evaluator": # 导入模块,这样子类就会被加载到内存 __import__(module_name) # 把模块里的类加到__all__里(可选,方便外部导入) module = sys.modules[module_name] for attr_name in dir(module): attr = getattr(module, attr_name) if isinstance(attr, type) and issubclass(attr, module.Evaluator) and attr != module.Evaluator: __all__.append(attr_name) # 初始化__all__ __all__ = []
这样以后新增评估器子类,只要放在evaluators目录下,不用改__init__.py就能被工厂类通过Evaluator.__subclasses__()获取到。
方案2:配置驱动的动态导入(更适合团队协作)
如果不想依赖自动导入,还可以用配置文件指定要加载的评估器类路径,让工厂类动态导入:
首先在你的evaluation_config里加一个配置项:
# 示例配置,可放在yaml/json或代码里 evaluation_config = { "enabled_evaluators": [ "evaluation.evaluators.score_evaluator.ScoreEvaluator", "evaluation.evaluators.feedback_evaluator.FeedbackEvaluator" ] }
然后修改EvaluatorFactory的_create_evaluators方法:
import importlib class EvaluatorFactory: # ... 其他代码 def _create_evaluators(self): evaluators = [] # 从配置中读取要加载的评估器类路径 for class_path in self.evaluation_config.get("enabled_evaluators", []): # 拆分模块名和类名 module_name, class_name = class_path.rsplit('.', 1) # 导入模块 module = importlib.import_module(module_name) # 获取类并实例化 evaluator_cls = getattr(module, class_name) evaluators.append(evaluator_cls(self.framework_config, self.evaluation_config, **self.kwargs)) return evaluators
这种方式的好处是:新增评估器不用改任何核心代码,只要在配置里加一行路径就行,完全符合开闭原则,团队协作时非常友好。
二、设计可扩展的评估器-指标关联架构
针对“特定指标属于特定评估器”的需求,我们可以用注册器模式或者配置映射来实现完全解耦的扩展方案:
方案1:用装饰器给评估器注册指标
首先确保你的Metric抽象类是标准的:
# metrics/metric.py from abc import ABC, abstractmethod class Metric(ABC): @abstractmethod def compute(self, predictions, targets): # 计算指标逻辑 pass @property @abstractmethod def name(self): # 返回指标名称,比如"QWK" pass
然后给每个评估器类添加一个注册器:
# evaluators/score_evaluator.py from evaluation.metrics.metric import Metric class ScoreEvaluator(Evaluator): # 类属性,存储该评估器的所有指标实例 _metrics = [] @classmethod def register_metric(cls, metric_cls): """装饰器:用来注册指标到当前评估器""" if issubclass(metric_cls, Metric): cls._metrics.append(metric_cls()) else: raise ValueError("必须注册Metric的子类") return metric_cls def __init__(self, evaluation_config, framework_config, **kwargs): super().__init__(evaluation_config, framework_config, **kwargs) self.score_generator = ScoreGenerator(llm=LLMClient(), vlm=VLMClient(), config=self.framework_config) # 初始化指标列表 self.metrics = self._metrics.copy() async def __call__(self, *args, **kwargs): # 假设这里已经拿到预测结果和真实目标 predictions, targets = await self._get_evaluation_data() # 计算所有指标 results = {} for metric in self.metrics: results[metric.name] = metric.compute(predictions, targets) return results
现在新增指标时,只要用评估器的装饰器注册就行,完全不用改评估器代码:
# metrics/QWK.py from evaluation.metrics.metric import Metric from evaluation.evaluators.score_evaluator import ScoreEvaluator @ScoreEvaluator.register_metric class QWK(Metric): @property def name(self): return "QWK" def compute(self, predictions, targets): # 具体计算QWK的逻辑 from sklearn.metrics import cohen_kappa_score return cohen_kappa_score(predictions, targets, weights="quadratic")
方案2:配置映射指标与评估器(更灵活)
如果不想在指标代码里硬绑定评估器,可以用配置来关联:
在evaluation_config里添加:
evaluation_config = { "evaluator_metrics": { "ScoreEvaluator": [ "evaluation.metrics.QWK.QWK", "evaluation.metrics.PCC.PCC" ], "FeedbackEvaluator": [ "evaluation.metrics.BLEU.BLEU" ] } }
然后修改评估器的__init__方法来动态加载指标:
# evaluators/score_evaluator.py import importlib class ScoreEvaluator(Evaluator): def __init__(self, evaluation_config, framework_config, **kwargs): super().__init__(evaluation_config, framework_config, **kwargs) self.score_generator = ScoreGenerator(llm=LLMClient(), vlm=VLMClient(), config=self.framework_config) # 从配置加载当前评估器的指标 metric_paths = evaluation_config.get("evaluator_metrics", {}).get(self.__class__.__name__, []) self.metrics = [] for path in metric_paths: module_name, class_name = path.rsplit('.', 1) module = importlib.import_module(module_name) metric_cls = getattr(module, class_name) self.metrics.append(metric_cls()) # ... 其他方法
这种方式的优势是:指标和评估器完全解耦,新增或修改关联关系只要改配置,不用动代码,适合复杂的多评估器多指标场景。
总结一下
- 子类加载:用自动导入或配置驱动的动态导入,告别手动维护
__init__.py的麻烦 - 指标扩展:用注册器装饰器或配置映射,让新增指标/评估器时不用修改核心代码,完全符合开闭原则
这样你的架构就能轻松应对团队后续的扩展需求啦!
备注:内容来源于stack exchange,提问作者Dinosaur

