简体   繁体   English

使用仅给定类型 object 的装饰器调用数据类构造函数

[英]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:一些微妙之处:

  • in __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
  • Have to use 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.可能有一种方法可以防止它强制执行,但这是我现在发现的。 I'm not very comfortable with it.我对此不太满意。 Hopefully someone can find a better way.希望有人能找到更好的方法。
  • had to use *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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM