使用类型对象调用带@validate_arguments装饰器的数据类构造器问题
问题分析与解决方案
问题描述
我有一个继承自抽象类的数据类,抽象类实现了样板代码,同时使用@validate_arguments装饰器在对象创建时自动将字符串转换为数值,部分字段在__post_init__中计算。但通过ReportEditor类接收Report子类类型并实例化时,装饰器的验证步骤未执行,导致字符串未转换而报错;直接调用子类构造器则正常工作。
代码复现
report.py
from abc import ABC, abstractmethod from dataclasses import dataclass, field from pydantic import validate_arguments @dataclass class Report(ABC): def __post_init__(self): self.process_attributes() @abstractmethod def process_attributes(self): pass @validate_arguments @dataclass class SpecificReport(Report): some_number: int some_other_number: float calculated_field: float = field(init=False) def process_attributes(self): self.calculated_field = self.some_number * self.some_other_number
report_editor.py
from inspect import signature from report import Report, SpecificReport class ReportEditor: def __init__(self, report_type: type[Report], content=None): self.content = content self.report_type = report_type self.explicit_fields = list(signature(report_type).parameters.keys()) def process(self): initializable_dict = {key: val for key, val in self.content.items() if key in self.explicit_fields} report = self.report_type(**initializable_dict) print(report)
main.py
from report import SpecificReport from report_editor import ReportEditor def example(): new_report = SpecificReport(1, 1.0) report_editor = ReportEditor(type(new_report), { "some_number": "1", "some_other_number": "1.0", "calculated_field": "1.0" }) report_editor.process() if __name__ == '__main__': example()
原因解析
- 装饰器作用对象问题:
@validate_arguments装饰的是数据类自动生成的构造函数,而非类本身。直接调用SpecificReport(...)时,实际调用的是被装饰后的构造函数;但通过self.report_type(**dict)调用时,本质是调用类的原生__init__方法,并没有经过装饰器的包装逻辑,因此类型转换不生效。 - 父类加装饰器报错原因:抽象类
Report无法直接实例化,@validate_arguments会试图包装其构造函数,但抽象类的构造逻辑与装饰器的实例化逻辑冲突,导致TypeError: cannot create 'cython_function_or_method' instances。
解决方案
方案一:改用Pydantic DataClass(推荐)
Pydantic的dataclass自带类型验证和自动转换功能,无需额外使用@validate_arguments,且能完美支持继承场景。修改report.py:
from abc import ABC, abstractmethod from pydantic.dataclasses import dataclass, field @dataclass class Report(ABC): def __post_init__(self): self.process_attributes() @abstractmethod def process_attributes(self): pass @dataclass class SpecificReport(Report): some_number: int some_other_number: float calculated_field: float = field(init=False) def process_attributes(self): self.calculated_field = self.some_number * self.some_other_number
此时ReportEditor无需修改,直接调用self.report_type(**initializable_dict)即可自动完成字符串到数值的转换,且验证逻辑生效。
方案二:手动调用装饰后的构造函数
如果坚持使用标准库dataclass,可将装饰后的构造函数保存为类属性,在ReportEditor中手动调用:
- 修改
report.py,为子类添加验证后的构造函数属性:
@dataclass class SpecificReport(Report): some_number: int some_other_number: float calculated_field: float = field(init=False) def process_attributes(self): self.calculated_field = self.some_number * self.some_other_number # 装饰__init__并保存为类属性 SpecificReport.__validated_init__ = validate_arguments(SpecificReport.__init__)
- 修改
ReportEditor的process方法:
def process(self): initializable_dict = {key: val for key, val in self.content.items() if key in self.explicit_fields} # 先创建空实例,再调用验证后的构造函数,最后执行post_init report = object.__new__(self.report_type) self.report_type.__validated_init__(report, **initializable_dict) report.__post_init__() print(report)
方案三:统一实例化入口(抽象类添加类方法)
在抽象类Report中定义一个类方法作为实例化入口,子类继承后自动应用验证逻辑:
@dataclass class Report(ABC): def __post_init__(self): self.process_attributes() @abstractmethod def process_attributes(self): pass @classmethod def create(cls, **kwargs): # 子类需确保__init__被@validate_arguments装饰 return cls(**kwargs)
然后ReportEditor的process方法改为调用create类方法:
def process(self): initializable_dict = {key: val for key, val in self.content.items() if key in self.explicit_fields} report = self.report_type.create(**initializable_dict) print(report)
注意:此方案需要每个子类的__init__都被@validate_arguments装饰,或者在create方法中统一应用装饰器(需处理子类构造函数的动态装饰)。
内容的提问来源于stack exchange,提问作者lunchdilemma
相关产品推荐
相关产品推荐

