Классы данных Python: какой тип использовать, если __post_init__ выполняет преобразование типов?
У меня есть класс Python, с полем, которому можно передать один из нескольких типов последовательности. Для упрощения я буду придерживаться кортежей и списков. __init__
преобразует параметр в MyList
,
from typing import Union
from dataclasses import dataclass, InitVar, field
class MyList(list):
pass
@dataclass
class Struct:
field: Union[tuple, list, MyList]
def __post_init__(self):
self.field = MyList(self.field)
Какой тип я должен использовать для field
декларация?
- Если я предоставлю объединение всех возможных типов ввода, код не документирует, что
field
всегдаMyList
при доступе. - Если бы я только поставил финал
MyList
типа, PyCharm жалуется, когда я прохожуStruct()
list
,
Я мог бы вместо этого использовать:
_field: InitVar[Union[tuple, list, MyList]] = None
field: MyList = field(init=False)
def __post_init__(self, _field):
self.field = MyList(_field)
но это ужасно некрасиво, особенно когда повторяется через 3 поля. Кроме того, я должен построить структуру, как Struct(_field=field)
вместо Struct(field=field)
,
В апреле 2018 года "tm" прокомментировала эту проблему в объявлении PyCharm: https://blog.jetbrains.com/pycharm/2018/04/python-37-introducing-data-class/.
3 ответа
Вы объединяете присвоение значения атрибуту с кодом, который создает значение для присвоения атрибуту. Я бы использовал отдельный метод класса, чтобы разделить два фрагмента кода.
from dataclasses import dataclass
class MyList(list):
pass
@dataclass
class Struct:
field: MyList
@classmethod
def from_iterable(cls, x):
return cls(MyList(x))
s1 = Struct(MyList([1,2,3]))
s2 = Struct.from_iterable((4,5,6))
Теперь вы передаете только существующее значение в
Struct.__init__
. Кортежи, списки и все остальное, что можно принять, передаются в
Struct.from_iterable
вместо этого, который позаботится о построении
MyList
экземпляр передать
Struct
.
dataclasses
лучше всего работает в простых контейнерах данных, расширенные утилиты, такие как преобразование, были сознательно опущены (см. здесь для полного описания этой и подобных функций). Реализация этого требует немалой работы, так как он также должен включать плагин pycharm, который замечает, насколько сейчас будет поддерживаться преобразование.
Гораздо лучшим подходом было бы использовать одну из сторонних разработчиков, которые уже сделали это, наиболее популярным из которых является , вероятно, потому, что у него самая простая миграция для классов данных .
Уроженец
pydantic
решение может выглядеть так, где код преобразования является частью
MyList
. Такое обращение делает
__post_init__
ненужные, что приводит к более чистым определениям модели:
import pydantic
class MyList(list):
@classmethod
def __get_validators__(cls):
"""Validators handle data validation, as well as data conversion.
This function yields validator functions, with the last-yielded
result being the final value of a pydantic field annotated with
this class's type.
Since we inherit from 'list', our constructor already supports
building 'MyList' instances from iterables - if we didn't, we
would need to write that code by hand and yield it instead.
"""
yield cls
class Struct(pydantic.BaseModel):
field: MyList # accepts any iterable as input
print(Struct(field=(1, 2, 3)))
# prints: field=[1, 2, 3]
Вы пробовали Pydantic BaseModel вместо класса данных?
Со следующим кодом мой Pycharm не жалуется:
from pydantic import BaseModel
class MyList(list):
pass
class PydanticStruct(BaseModel):
field: MyList
def __post_init__(self):
self.field = MyList(self.field)
a = PydanticStruct(field=['a', 'b'])