[英]Calling a dataclass constructor with decorator given only a type object
I have a dataclass which inherits an abstract class that implements some boilerplate, and also uses the @validate_arguments
decorator to immediately cast strings back into numbers on object creation.我有一个数据类,它继承了一个抽象的 class,它实现了一些样板,并且还使用
@validate_arguments
装饰器在创建 object 时立即将字符串转换回数字。 The dataclass is a series of figures, some of which are calculated in the __post_init__
.数据类是一系列数字,其中一些是在
__post_init__
中计算的。
report.py
: 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
I then have another class which is initialized with a class of type Report
, gathers some metadata on creation about that class, and then has methods which perform operations with these objects, including taking some content and then constructing new objects of this type from a dictionary.然后我有另一个 class,它用
Report
类型的 class 初始化,在创建时收集一些关于该 class 的元数据,然后有对这些对象执行操作的方法,包括获取一些内容,然后从字典构造这种类型的新对象. We determine which fields are set explicitly with inspect.signature
and explode out our dictionary and call the constructor.我们确定哪些字段是用
inspect.signature
显式设置的,然后展开我们的字典并调用构造函数。
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)
However, this produces an error when hitting process_attributes
, because the validate_arguments
step is not performed.但是,这会在命中
process_attributes
时产生错误,因为未执行validate_arguments
步骤。 Aside from that, the object is initialized as I'd expect, but since the values are strings, they remain as such and only throw an exception once trying to do an operation.除此之外,object 已按照我的预期进行了初始化,但由于值是字符串,因此它们保持原样并且仅在尝试执行操作时抛出异常。
This works just fine and produces the desired behavior:这工作得很好并产生所需的行为:
def process(self):
initializable_dict = {key: val for key, val in self.content.items() if key in self.explicit_fields}
report = SpecificReport(**initializable_dict)
print(report)
but, of course, the intent is to abstract that away and allow this ReportEditor
class to be able to do these operations without knowing what kind of Report
it is.但是,当然,目的是将其抽象化,并允许此
ReportEditor
class 能够在不知道它是哪种Report
的情况下执行这些操作。
here is main.py
to run the reproducible example:这是运行可重现示例的
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()
I tried putting @validate_arguments on both the parent and child classes, as well as only on the parent Report
class. These both resulted in a TypeError: cannot create 'cython_function_or_method' instances
.我尝试将 @validate_arguments 放在父类和子类上,以及仅放在父
Report
class 上。这些都导致了TypeError: cannot create 'cython_function_or_method' instances
。 I'm not finding any other way there is to call the constructor from outside just using the type
object.我没有找到任何其他方法可以仅使用
type
object 从外部调用构造函数。
Why is the constructor called properly, but not the decorator function in this instance?为什么在这个实例中正确调用了构造函数,但没有调用装饰器 function? Is it possible to maybe cast a
type
object to a Callable
in order to get the full constructor somehow?是否有可能将
type
object 转换为Callable
以便以某种方式获得完整的构造函数? What am I missing?我错过了什么? Or is this just not possible (maybe with generics)?
或者这是不可能的(也许使用泛型)?
Here is the fundamental problem:这是根本问题:
In [1]: import report
In [2]: new_report = report.SpecificReport(1, 1.0)
In [3]: type(new_report) is report.SpecificReport
Out[3]: False
This is happening because the pydantic.validate_arguments
decorator returns a cythonized function:发生这种情况是因为
pydantic.validate_arguments
装饰器返回一个 cythonized function:
In [4]: report.SpecificReport
Out[4]: <cyfunction SpecificReport at 0x1103bb370>
The function does the validation. function进行验证。 The class constructor doesn't.
class 构造函数没有。 It looks like this decorator is experimental, and at least for now, is not designed to work on classes (it just happens to work since a class is just a callable with
.__annotations__
after all).看起来这个装饰器是实验性的,至少现在,它不是为类工作而设计的(它恰好可以工作,因为毕竟 class 只是一个可调用的
.__annotations__
)。
EDIT:编辑:
However, if you do want validation, you can use pydantic.dataclasses
, which is a "drop-in" (not quite but drop-in by very close and they made a real effort at compatibility) replacement for the standard library dataclasses
.但是,如果您确实想要验证,则可以使用
pydantic.dataclasses
,它是标准库dataclasses
的“插入式”(不完全是插入式,但他们在兼容性方面做出了真正的努力)替代品。 You can use change the report.py
to the following:您可以使用将
report.py
更改为以下内容:
from abc import ABC, abstractmethod
import dataclasses
import pydantic
@pydantic.dataclasses.dataclass
class Report(ABC):
def __post_init_post_parse__(self, *args, **kwargs):
self.process_attributes()
@abstractmethod
def process_attributes(self, *args, **kwargs):
pass
@pydantic.dataclasses.dataclass
class SpecificReport(Report):
some_number: int
some_other_number: float
calculated_field: dataclasses.InitVar[float] = dataclasses.field(init=False)
def process_attributes(self, *args, **kwargs):
self.calculated_field = self.some_number * self.some_other_number
Some subtleties:一些微妙之处:
__post_init__
, the arguments haven't been parsed and validated, but you can use __post_init_post_parse__
if you want them validated/parsed.__post_init__
中,arguments 还没有被解析和验证,但是如果你想让它们被验证/解析,你可以使用__post_init_post_parse__
。 We do, or else self.some_number * self.some_other_number
will raise the TypeError
self.some_number * self.some_other_number
将引发TypeError
dataclasses.InitVar
along with dataclasses.field(init=False)
because without InitVar
, the validation fails if __post_init__
didn't set calculated_field
(so we can't use the parsed fields in __post_init_post_parse__
because the missing attribute is checked earlier ).dataclasses.InitVar
和 dataclasses.field dataclasses.field(init=False)
因为没有InitVar
,如果__post_init__
没有设置calculated_field
验证失败(所以我们不能在__post_init_post_parse__
中使用解析的字段,因为缺少的属性在前面检查过) . There might be a way to prevent it from enforcing that, but this is what I found for now.*args, **kwargs
in __post_init_post_parse__
and in process
because InitVar
will pass an argument, so extenders of this class might want to do the same, so make it generic.__post_init_post_parse__
和process
中使用*args, **kwargs
,因为InitVar
将传递一个参数,所以这个 class 的扩展者可能想要做同样的事情,所以让它成为通用的。 I had to add **args, **kwargs
to the我必须添加
**args, **kwargs
到
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.