Каждому разработчику Python требовалось в какой-то момент создать класс, предназначенный для хранения данных. До сих пор при реализации такого класса им приходилось реализовывать многие базовые функции, такие как хеширование и сравнение, что в итоге приводило к груде шаблонного кода. Вот тут-то и пригодятся классы данных, чтобы упростить и сделать их более компактными.
Что такое класс данных?
Класс данных, как следует из его названия, - это класс, обычно используемый для хранения данных.
Классы данных содержат только поля и методы для доступа к ним (геттеры и сеттеры). Они просто действуют как контейнеры данных, используемые другими классами.
Классы данных в Python
Начиная с python 3.7, была представлена новая захватывающая функция - декоратор @dataclass
через библиотеку Dataclasses
.
Декоратор @dataclass
используется для автоматического создания базовых функций для классов, включая __init__()
, __hash__()
, __repr__()
и другие, что помогает сократить некоторый шаблонный код.
Например, украшение класса @dataclass
позволит вам создавать, печатать и сравнивать его экземпляры прямо из коробки:
from dataclasses import dataclass | |
@dataclass | |
class Person: | |
name: str | |
age: int | |
>>> person = Person('John', 41) | |
>>> person.name | |
'John' | |
>>> person.age | |
41 | |
>>> person | |
Person(name='John', age=41) | |
>>> person == Person('John', 41) | |
True |
Классы данных можно рассматривать как «изменяемые именованные кортежи со значениями по умолчанию». Поскольку классы данных используют обычный синтаксис определения классов, вы можете свободно использовать 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 |
Теперь давайте реализуем тот же класс с теми же функциями, но на этот раз с использованием 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
можно еще много поговорить, но это уже другая статья.