У меня есть класс данных, который наследует абстрактный класс, который реализует некоторый шаблон, а также использует декоратор @validate_arguments
для немедленного преобразования строк обратно в числа при создании объекта. Класс данных представляет собой серию цифр, некоторые из которых рассчитываются в __post_init__
.
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
, собирает некоторые метаданные при создании об этом классе, а затем имеет методы, которые выполняют операции с этими объектами, включая получение некоторого содержимого и последующее создание новых объектов этого типа из словаря. . Мы определяем, какие поля заданы явно с помощью 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)
Однако это приводит к ошибке при нажатии process_attributes
, потому что шаг validate_arguments
не выполняется. Кроме того, объект инициализируется, как я и ожидал, но, поскольку значения являются строками, они остаются такими и вызывают исключение только при попытке выполнить операцию.
Это прекрасно работает и дает желаемое поведение:
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)
но, конечно, цель состоит в том, чтобы абстрагироваться от этого и позволить этому ReportEditor
классу выполнять эти операции, не зная, что это за 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 как в родительский, так и в дочерний классы, а также только в родительский класс Report
. Оба они привели к TypeError: cannot create 'cython_function_or_method' instances
. Я не нахожу другого способа вызвать конструктор извне, просто используя объект type
.
Почему в данном случае правильно вызывается конструктор, а не функция декоратора? Возможно ли привести объект type
к Callable
, чтобы каким-то образом получить полный конструктор? Что мне не хватает? Или это просто невозможно (может быть, с дженериками)?
крики. Итак, это явно как-то связано с декоратором pydantic.validate_arguments
. проблема в том, что type(SpecificReport(1, 1.0)) is report.SpecificReport
есть False
.
он должен возвращать что-то отличное от типа - если вы посмотрите print(report.SpecificReport)
, вы увидите, что это <cyfunction SpecificReport at 0x103c581e0>
. Это проблема, которая возникает, когда декораторы класса не возвращают сам класс!
Актуальный класс доступен по адресу: report.SpecificReport.raw_function
.
Так что, по сути, это фундаментальный недостаток реализации pydantic.validate_arguments
, по крайней мере, в том, что касается декорирования классов, поскольку на самом деле он не возвращает класс, а возвращает функцию (которая предположительно оборачивает вызов самого класса).
Я даже не уверен, что validate_arguments
предназначен для работы с классами, просто это работает, потому что класс является вызываемым, а __annotations__
является правильной сигнатурой __init__
Это объясняет TypeError: cannot create 'cython_function_or_method' instances
при размещении декоратора в родительском классе! Похоже, он пытался наследоваться от функции.
да, это было бы вполне возможно. Опять же, не похоже, что они когда-либо предназначали его для украшения классов, иначе он должен был бы работать как dataclasses.dataclass
, который просто возвращает сам класс. В документах говорится, что на данном этапе это экспериментально. Вы можете просто использовать pydantic.dataclasses
, если вам просто нужен класс данных с проверкой/принуждением
Вот основная проблема:
In [1]: import report
In [2]: new_report = report.SpecificReport(1, 1.0)
In [3]: type(new_report) is report.SpecificReport
Out[3]: False
Это происходит потому, что декоратор pydantic.validate_arguments
возвращает цитированную функцию:
In [4]: report.SpecificReport
Out[4]: <cyfunction SpecificReport at 0x1103bb370>
Функция выполняет проверку. Конструктор класса этого не делает. Похоже, что этот декоратор является экспериментальным и, по крайней мере, на данный момент не предназначен для работы с классами (просто так случилось, что класс — это просто вызываемый объект с .__annotations__
).
Обновлено:
Однако, если вам нужна проверка, вы можете использовать pydantic.dataclasses
, которая является «вставной» (не совсем, но очень близкой, и они приложили реальные усилия для совместимости) заменой стандартной библиотеки dataclasses
. Вы можете изменить 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
Некоторые тонкости:
__post_init__
аргументы не были проанализированы и проверены, но вы можете использовать __post_init_post_parse__
, если хотите, чтобы они были проверены/проанализированы. Мы делаем, иначе self.some_number * self.some_other_number
поднимет TypeError
dataclasses.InitVar
вместе с dataclasses.field(init=False)
, потому что без InitVar
проверка завершается ошибкой, если __post_init__
не установлено calculated_field
(поэтому мы не можем использовать проанализированные поля в __post_init_post_parse__
, потому что отсутствующий атрибут проверяется раньше). Возможно, есть способ предотвратить это, но это то, что я нашел на данный момент. Мне это не очень удобно. Надеюсь, кто-то может найти лучший способ.*args, **kwargs
в __post_init_post_parse__
и в process
, потому что InitVar
будет передавать аргумент, поэтому расширители этого класса могут захотеть сделать то же самое, поэтому сделайте его универсальным.Пришлось добавить **args, **kwargs
к
Это более или менее то, с чем я пошел, хотя вместо того, чтобы использовать InitVar
s, я присвоил значение по умолчанию 0 каждому из вычисляемых полей - есть некоторое использование fields()
в другом месте кода, предназначенного для включения вычисляемых полей, поэтому InitVar
не будет работать в этом сценарии. В остальном это сработало отлично!
Хорошо, я только что обновил вопрос с полным кодом примера, я проверил это в черновом проекте и подтвердил, что проблема воспроизводима с добавлением трех файлов, которые я отредактировал. С
report = self.report_type(**initializable_dict)
все атрибуты инициализируются, но как строки, и я получаюTypeError: can't multiply sequence by non-int of type 'str'
, но сreport = SpecificReport(**initializable_dict)
он прекрасно проходит через методprocess_attributes()
и через оператор печати и печатает ожидаемый объект.