Классы данных 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'])
Другие вопросы по тегам