Каждому разработчику Python требовалось в какой-то момент создать класс, предназначенный для хранения данных. До сих пор при реализации такого класса им приходилось реализовывать многие базовые функции, такие как хеширование и сравнение, что в итоге приводило к груде шаблонного кода. Вот тут-то и пригодятся классы данных, чтобы упростить и сделать их более компактными.

Что такое класс данных?

Класс данных, как следует из его названия, - это класс, обычно используемый для хранения данных.
Классы данных содержат только поля и методы для доступа к ним (геттеры и сеттеры). Они просто действуют как контейнеры данных, используемые другими классами.

Классы данных в Python

Начиная с python 3.7, была представлена ​​новая захватывающая функция - декоратор @dataclass через библиотеку Dataclasses.

Декоратор @dataclass используется для автоматического создания базовых функций для классов, включая __init__(), __hash__(), __repr__() и другие, что помогает сократить некоторый шаблонный код.

Например, украшение класса @dataclass позволит вам создавать, печатать и сравнивать его экземпляры прямо из коробки:

from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
view raw person.py hosted with ❤ by GitHub
>>> person = Person('John', 41)
>>> person.name
'John'
>>> person.age
41
>>> person
Person(name='John', age=41)
>>> person == Person('John', 41)
True
view raw python_bash.py hosted with ❤ by GitHub

Классы данных можно рассматривать как «изменяемые именованные кортежи со значениями по умолчанию». Поскольку классы данных используют обычный синтаксис определения классов, вы можете свободно использовать inheritance, metaclasses, docstrings, определяемые пользователем методы, фабрики классов и другие функции классов Python.

Более подробный пример магии Dataclasses.

Давайте реализуем класс, представляющий книгу обычным способом.
Эта книга будет неизменной и сопоставимой с другими книгами.

class Book:
def __init__(self, name, author, num_pages):
self.__name = name
self.__author = author
self.__num_pages = num_pages
@property
def name(self):
return self.__name
@property
def author(self):
return self.__author
@property
def num_pages(self):
return self.__num_pages
def __repr__(self):
return f'Book({self.name}, {self.author}, {self.num_pages})'
def __hash__(self):
return hash((self.__class__, self.name, self.author, self.num_pages))
def __eq__(self, other):
if other.__class__ is self.__class__:
return self.name == other.name and self.author == other.author and self.num_pages == other.num_pages
else:
return NotImplemented
view raw book.py hosted with ❤ by GitHub

Теперь давайте реализуем тот же класс с теми же функциями, но на этот раз с использованием Dataclasses.

from dataclasses import dataclass
@dataclass(frozen=True)
class Book:
name: str
author: str
num_pages: int

Итак, как видите, с 30 строк кода осталось всего 7 строк (считая пустые строки для удобства чтения). Также представьте, что вы хотите добавить к классу новый атрибут. Используя обычный способ, вам нужно будет реорганизовать все методы, чтобы учесть новый атрибут в каждом из них, тогда как с Dataclasses вам нужно будет добавить еще одну строку с новый атрибут.

Параметры декоратора класса данных

Декоратор @dataclass имеет несколько параметров, которые вы можете использовать (например, параметр frozen в приведенном выше примере), эти параметры определяют характер класса:

  • init (True по умолчанию) сгенерирует метод __init__, который, в свою очередь, инициализирует каждый атрибут в соответствии с его объявлением.
  • repr (True по умолчанию) сгенерирует метод __repr__, который, в свою очередь, сгенерирует строку повторения, которая будет иметь имя класса, имя и имя каждого поля в том порядке, в котором они определены в классе. Поля, отмеченные как исключенные из репортажа, не будут включены. Например: Книга (имя = ’Хоббит’, автор = ’Дж. Р. Р. Толкин’, num_pages = 310).
  • eq (True по умолчанию) сгенерирует метод __eq__, который по порядку сравнивает класс, как если бы он был кортежем его полей.
  • order (False по умолчанию), чтобы генерировать методы __lt__, __le__, __gt__ и __ge__, которые позволяют сравнивать экземпляры классов. Если order равен true, а eq - false, возникает ошибка ValueError.
  • frozen (False по умолчанию), чтобы эмулировать замороженные экземпляры, доступные только для чтения. Другими словами, если True, делает объекты неизменяемыми (присвоение полей вызовет исключение), и поэтому они могут использоваться как ключи словаря.
  • unsafe_hash (False по умолчанию) определяет, как __hash__ реализуется в соответствии с eq и frozen. Если eq и frozen оба равны True, классы данных сгенерируют для вас __hash__ метод. Если eq равно True, а frozen равно False, __hash__ будет установлено в None, отмечая его как нехешируемый (что так и есть). Если eq равно False, __hash__ останется нетронутым, что означает, что будет использоваться метод __hash__ суперкласса (если суперкласс object, это означает, что он вернется к хешированию на основе идентификатора).

Поля класса данных

Существует возможность установить значения по умолчанию для полей класса данных:

from dataclasses import dataclass, field
@dataclass
class Book:
name: str # similar to - name: str = field()
author: str # same here...
condition: str = field(default='new') # similar to condition: str = 'new'
readers: list = field(default_factory=list, compare=False, hash=False, repr=False)

В некоторых случаях достаточно «простого» пути и использования condition: str = 'new', но в других случаях, когда для инициализации поля требуется более сложный метод, самое время использовать метод field().

Представьте, что вы хотите инициализировать поле типа List.
Использование field_name = [] или field_name = list() не поможет, поскольку конечным результатом их использования является список mutable, который будет использоваться всеми экземплярами класса:

from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
friends: list = list()
>>> person1 = Person('John', 41)
>>> person2 = Person('Lisa', 39)
>>> person1
Person(name='John', age=41)
>>> person2
Person(name='Lisa', age=39)
>>> person1.friends
[]
>>> person2.friends
[]
>>> person1.friends.append('Debby')
>>> person1.friends
['Debby']
>>> person2.friends
['Debby']

В приведенном выше случае правильный способ - инициализировать поле friends (или любое другое поле типа List):

from dataclasses import dataclass, field
@dataclass
class Person:
name: str
age: int
friends: list = field(default_factory=list)

И конечный результат:

>>> person1 = Person('John', 41)
>>> person2 = Person('Lisa', 39)
>>> person1.friends
[]
>>> person2.friends
[]
>>> person1.friends.append('Debby')
>>> person1.friends
['Debby']
>>> person2.friends
[]

Параметры метода field():

  • default: Если указано, это будет значение по умолчанию для этого поля.
  • default_factory: Если указано, это должен быть вызываемый объект без аргументов, который будет вызываться, когда для этого поля потребуется значение по умолчанию. Среди прочего, это можно использовать для указания полей с изменяемыми значениями по умолчанию.
  • init: Если True (по умолчанию), это поле включается в качестве параметра в сгенерированный __init__method.
  • repr: если True (по умолчанию), это поле включается в строку, возвращаемую сгенерированным методом __repr__.
  • compare: Если True (по умолчанию), это поле включается в сгенерированные методы сравнения и сравнения (__eq__, __gt__ и т. Д.).
  • hash: это может быть bool или None. Если True, это поле включается в сгенерированный __hash__ метод. Если None (по умолчанию), используйте значение compare.

Примечание. Одна из возможных причин для установки hash = False, но compare = True может заключаться в том, что для поля слишком дорого вычислять хеш-значение, это поле необходимо для проверки равенства, и есть другие поля, которые влияют на хэш типа. ценить. Даже если поле исключено из хеша, оно все равно будет использоваться для сравнения.

Заключение

При работе с классами, ориентированными на данные, Dataclasses - отличный инструмент.
С классами данных вам не нужно писать шаблонный код для правильной инициализации, представления и сравнения ваших объектов. Это поможет вам избежать лишнего набора текста и размышлений, сделав код менее подверженным ошибкам, поскольку он уже делает почти все за вас.
Могут быть добавлены и другие методы, как и в любом другом классе, но рекомендуется оставить основные функции под управлением Dataclasses.

О Dataclasses можно еще много поговорить, но это уже другая статья.