DataClass в Python

Конструкции dataclass доступны в Python 3.7 и старше. Их можно использовать как контейнеры данных и не только. Конструкции dataclass позволяют писать шаблонный код и упрощают процесс создания классов, ведь в них для этого есть специальные методы.

Первый Dataclass

Создадим DataClass, представляющий собой точку в трехмерной системе координат.

Декоратор @dataclass используется для создания Data Class. x, y и z являются его полями. Стоит отметить, что с ними необходимо использовать определения типов данных. При этом они не являются объявлениями статического типа, и кто угодно может передать данные любого типа кроме int полям x, y или z.

from dataclasses import dataclass


@dataclass
class Coordinate:
    x: int
    y: int
    z: int

По умолчанию у Data Class есть методы __init__, __repr__ и __eq__, поэтому их не нужно реализовывать самостоятельно.

И пусть __init__, __repr__ и __eq__ не реализованы в классе Coordinate их по-прежнему можно использовать благодаря dataclass. Это здорово экономит время.

from dataclasses import dataclass


@dataclass
class Coordinate:
    x: int
    y: int
    z: int


a = Coordinate(4, 5, 3)
print(a)  # результат: Coordinate(x=4, y=5, z=3)

Значения по умолчанию для полей

Полям можно присваивать значения по умолчанию. Используем пример. Как видно по полю pi, можно запросто присвоить значение по умолчанию для полей в DataClass.

from dataclasses import dataclass


@dataclass
class CircleArea:
    r: int
    pi: float = 3.14

    @property
    def area(self):
        return self.pi * (self.r ** 2)


a = CircleArea(2)
print(repr(a))  # вернется: CircleArea(r=2, pi=3.14)
print(a.area)  # вернется: 12.56

Изменение полей и DataClass

Изменять поля и DataClass можно за счет параметров декоратора или функции поля. Все случаи будут рассмотрены в примерах.

Data Class: изменяемые или нет?

По умолчанию dataclass изменяемые, что значит, что полям можно присваивать значения. Но их же можно сделать и неизменяемыми, задав значение True для параметра frozen.

Изменяемый пример

from dataclasses import dataclass


@dataclass
class CircleArea:
    r: int
    pi: float = 3.14

    @property
    def area(self):
        return self.pi * (self.r ** 2)


a = CircleArea(2)
a.r = 5
print(repr(a))  # вернется: CircleArea(r=5, pi=3.14)
print(a.area)  # вернется: 78.5

Неизменяемый пример

Когда значение frozen равно True, присваивать значения полям больше нельзя. Дальше показано соответствующее исключение.

from dataclasses import dataclass


@dataclass(frozen=True)
class CircleArea:
    r: int
    pi: float = 3.14

    @property
    def area(self):
        return self.pi * (self.r ** 2)


a = CircleArea(2)
a.r = 5
# Вернется исключение: dataclasses.FrozenInstanceError:
# cannot assign to field 'r'

Сравнение Data Classes

Представьте, что вы хотите создать DataClass, представляющий Vector и сравнить их. Как это сделать? Для сравнения нужны методы __lt__ или __gt__.

По умолчанию значение параметра order в Data Class равняется False. Если поменять его на True, то методы __lt__, __le__, __gt__ и __ge__ для Data Class будут сгенерированы автоматически. Таким образом можно сравнить объекты так, будто бы они являются кортежами полей.

Разберем на примере. Задав order значение True, можно сравнить v2 и v1. Но есть проблема логики сравнения. Если сравнить, например, v2 > v1, то векторы будут сравниваться вот так: (8, 15) > (7, 20). Таким образом значением операции будет True.

Стоит напомнить, что сравнение кортежей происходит по порядку. Сначала сравниваются 8 и 7, и если результат равен True, то результатом всего сравнения будет True. Если бы они были равны, то произошло бы сравнение 15 > 20 и результат стал бы False.

from dataclasses import dataclass, field


@dataclass(order=True)
class Vector:
    x: int
    y: int


v1 = Vector(8, 15)
v2 = Vector(7, 20)
print(v2 > v1)

Очевидно, что в таком сравнении нет никакого смысла. Правильнее было бы сравнивать величину векторов. Проблема в том, что не хотелось бы высчитывать эту величину самостоятельно при создании экземпляров.

В этом случае можно воспользоваться функцией field и методом __post_init__. Функция field позволит настроить поле magnitude, а __post_init__ — определить величину вектора после инициализации.

Поле magnitude было изменено с помощью функции field из Data Class. Задавая значение False для init, мы утверждаем, что не хотим иметь параметр magnitude в методе __init__. Потому что его значение можно задать с помощью метода __post_init__ после инициализации.

Конвертация в словарь или кортеж

Можно получить атрибуты Data Class в кортеже или словаре. Для этого нужно лишь импортировать функции asdict и astuple из Data Class.

from dataclasses import dataclass, asdict, astuple


@dataclass
class Vector:
    x: int
    y: int
    z: int


v = Vector(4, 5, 7)
print(asdict(v))  # Вернется : {'x': 4, 'y': 5, 'z': 7}
print(astuple(v))  # Вернется : (4, 5, 7)

Наследование

Можно создавать подклассы для Data Class как для обычных классов в Python.

from dataclasses import dataclass


@dataclass
class Employee:
    name: str
    lang: str


@dataclass
class Developer(Employee):
    salary: int


Alex = Developer('Alex', 'Python', 5000)
print(Alex)  # Вернется: Developer(name='Alex', lang='Python', salary=5000)

Но есть распространенная ошибка при использовании наследования. Когда значением поля lang по умолчанию является Python, нужно задавать значения по умолчанию для полей следом за lang.

from dataclasses import dataclass


@dataclass
class Employee:
    name: str
    lang: str = 'Python'


@dataclass
class Developer(Employee):
    salary: int


Alex = Developer('Alex', 'Python', 5000)
# Вернется: TypeError: non-default argument 'salary' follows default argument

Чтобы понять, зачем это нужно, рассмотрим, как выглядит метод __init__. Вспомним, что параметры со значением по молчанию всегда идут после параметров без них.

def __init__(name: str, lang: str = 'Python', salary: int):
    ...

Исправим, задав значение по умолчанию для поля salary.

from dataclasses import dataclass


@dataclass
class Employee:
    name: str
    lang: str = 'Python'


@dataclass
class Developer(Employee):
    salary: int = 0


Alex = Developer('Alex', 'Python', 5000)
print(Alex)  # Вернется: Developer(name='Alex', lang='Python', salary=5000)

Преимущества слотов

По умолчанию атрибуты хранятся в словаре. Можно получить преимущество от слотов для более быстрого доступа к атрибутам и использовать меньше памяти. Однако в подробностях эта тема здесь рассматриваться не будет.

from dataclasses import dataclass


@dataclass
class Employee:
    name: str
    lang: str


Alex = Employee('Alex', 'Python')
print(Alex.__dict__)  # {name': 'Alex', 'lang': 'Python'}

Слоты нужны для использования меньшего количества памяти и более быстрого доступа к атрибутам.

from dataclasses import dataclass


@dataclass
class Employee:
    __slots__ = ('name', 'lang')
    name: str
    lang: str


Alex = Employee('Alex', 'Python')

Параметры Dataclass

Некоторые параметры в декораторе Data Class были изменены для настройки Data Class. Вот они:

  • init: метод __init__ будет сгенерирован в Data Class при значении True. (True – значение по умолчанию)
  • repr: метод __repr__ будет сгенерирован в Data Class при значении True. (True – значение по умолчанию)
  • eq: метод __eq__ будет сгенерирован в Data Class при значении True. (True – значение по умолчанию)
  • order: методы __lt__, __le__, __gt__ и __ge__ будут сгенерированы в Data Class при значении True. (False – значение по умолчанию)
  • unsafe_hash: метод __hash__ будет сгенерирован в Data Class при значении True. (False – значение по умолчанию)
  • frozen: если True, то значения полям присваивать нельзя (False – значение по умолчанию).

Примечание: eq должно быть равно True, если значение order равняется True, иначе будет исключение ValueError.

Параметры полей

  • init: это поле включено в сгенерированный метод __init__ при значении True (True – значение по умолчанию)
  • repr: это поле включено в сгенерированный метод __init__ при значении True (True – значение по умолчанию)
  • compare: это поле включено в методы общего сравнения и равенства при значении True (True – значение по умолчанию)
  • hash: это поле включено в сгенерированный метод __hash__ при значении True (None – значение по умолчанию)
  • default: это будет значение по умолчанию для поля (если указано)
  • default_factory: если указано, должно быть вызываемым объектом без аргументов, который вызывается, когда для поля требуется значение по умолчанию
  • metadata: это может быть мапинг или None – пустой словарь.