Датаклассы в языке питон
0 – Dataclasses: общая картина
Статья более чем полностью скопированна с сайта habr.com
Оригинал текста
Dataclasses
, как ясно следует из названия – это классы, предназначенные для хранения данных. Основная идея заключается в том, что мы иногда определяем классы, которые действуют только как контейнеры данных, и когда так случается, мы тратим значительное количество времени на написание шаблонного кода с кучей аргументов, уродливым методом init и множеством переопределенных функций.
Dataclasses
решают эту проблему, предоставляя из коробки ряд дополнительных полезных методов. Более того, поскольку dataclasses
относительно новые для экосистемы Python, в них применяются современные практики, такие как аннотации типов.
Dataclasses
остаются классами. Поэтому в них вы можете реализовать любые кастомные методы точно также, как в обычном классе. А теперь посмотрим на них в действии.
1 – Меньше кода для определения класса
Вот так выглядит предыдущий код с использованием dataclasses:
from dataclasses import dataclass
@dataclass
class Person:
first_name: str
last_name: str
age: int
job: str
2 – Поддержка значений по умолчанию
Вы можете добавить значения по умолчанию для каждого атрибута, сохранив аннотацию.
from dataclasses import dataclass
@dataclass
class Person:
first_name: str = "Ahmed"
last_name: str = "Besbes"
age: int = 30
job: str = "Data Scientist"
Помните о том, что поля без значений по умолчанию не могут стоять после полей со значениями по умолчанию. Например, следующий код работать не будет:
from dataclasses import dataclass
@dataclass
class Person:
first_name: str = "Ahmed"
last_name: str = "Besbes"
age: int = 30
job: str = "Data Scientist"
hobbies: str
3 – Кастомное представление объектов
Благодаря методу repr
, добавленному dataclasses
, экземпляры имеют приятное, удобочитаемое представление при выводе на экран.
Да и отладка так становится проще.
@dataclass
class Person:
first_name: str = "Ahmed"
last_name: str = "Besbes"
age: int = 30
job: str = "Data Scientist"
ahmed = Person()
print(ahmed)
# Person(first_name='Ahmed', last_name='Besbes', age=30, job='Data Scientist')
Представление можно переопределить для вывода любого кастомного сообщения, которое вам может понадобиться.
@dataclass
class Person:
first_name: str = "Ahmed"
last_name: str = "Besbes"
age: int = 30
job: str = "Data Scientist"
def __repr__(self):
return f"{self.first_name} {self.last_name} ({self.age})"
ahmed = Person()
print(ahmed)
# Ahmed Besbes (30)
4 – Упрощенная конвертация в кортеж или словарь
Экземпляры можно легко сериализовать в словари или кортежи. Механика оказывается полезной, когда ваш код взаимодействует с другими программами, которые ожидают именно эти типы.
from dataclass import astuple, asdict
ahmed = Person()
print(astuple(ahmed)
# ('Ahmed', 'Besbes', 30, 'Data Scientist')
print(asdict(ahmed)
# {'first_name': 'Ahmed',
# 'last_name': 'Besbes',
# 'age': 30,
# 'job': 'Data Scientist'}
5 – Замороженные экземпляры/неизменяемые объекты
С помощью dataclasses
можно создавать объекты, доступные только для чтения. Все, что нужно сделать – установить значение frozen
в True внутри декоратора @dataclass
перед нужным классом.
@dataclass(frozen=True)
class Person:
first_name: str = "Ahmed"
last_name: str = "Besbes"
age: int = 30
job: str = "Data Scientist"
Как только вы так сделаете, вы запретите кому-либо изменять значения атрибутов
после создания экземпляра класса.
Если вы попробуете установить для атрибута замороженного экземпляра
новое значение, получить ошибку FrozenInstanceError
.'
6 – Не нужно писать методы сравнения
Когда вы определяете класс с использованием стандартного синтаксиса Python
и прописываете равенство между двумя экземплярами,
имеющими одинаковые значения атрибутов, вы получаете следующую картину:
class Person():
def __init__(self, first_name, last_name, age, job):
self.first_name = first_name
self.last_name = last_name
self.age = age
self.job = job
first_person = Person("Ahmed", "Besbes", 30, "Data scientist")
second_person = Person("Ahmed", "Besbes", 30, "Data scientist")
print(first_person == second_person)
# False ❌
Два объекта не равны и это нормально, поскольку класс Person
фактически не реализует метод проверки равенства.
Чтобы прописать равенство, вам придется самостоятельно реализовать
метод eq . Выглядеть он может так:
def __eq__(self, other):
if other.__class__ is not self.__class__:
return NotImplemented
return (self.first_name,
self.last_name,
self.age,
self.job) == (other.first_name,
other.last_name,
other.age,
other.job)
Метод сначала проверяет, что два объекта являются экземплярами одного и того же класса, а затем проверяет равенство между кортежами атрибутов.
Теперь, если вы захотите добавить новые атрибуты в класс, вам придется снова обновлять метод eq. Аналогично с методами ge ,gt , le и lt , е сли они используются.
Очень похоже на лишний код, не так ли? К счастью, dataclasses и тут нам помогут.
@dataclass
class Person:
first_name: str = "Ahmed"
last_name: str = "Besbes"
age: int = 30
job: str = "Data Scientist"
first_person = Person()
second_person = Person()
print(first_person == second_person)
# True ✅
7 – Настраиваемое поведение атрибута с функцией field
Иногда вам может потребоваться атрибут, который определяется изнутри, а не при создании экземпляра класса. Такое бывает, если атрибут имеет значение, зависящее от ранее заданных атрибутов.
Тут вам на помощь приходит функция field
из dataclasses
.
С помощью этой функции и установки аргументов itsinit
и repr
в False
при создании нового поля с именем full_name
, мы все равно сможем создать
экземпляр класса Person
без установки атрибута full_name
.
from dataclasses import dataclass, field
@dataclass
class Person:
first_name: str = "Ahmed"
last_name: str = "Besbes"
age: int = 30
job: str = "Data Scientist"
full_name: str = field(init=False, repr=False)
Этого атрибута еще нет в экземпляре.
Если мы попробуем получить к нему доступ, то получим AttributeError
.
Как установить значение full_name
и при этом оставить
его снаружи конструктора класса?
Для этого нам придется использовать метод __post_init__
.
8 – Метод post_init
В dataclasses есть специальный метод __post_init__
.
Как следует из названия, этот метод вызывается сразу после метода __init__
.
Возвращаясь к предыдущему примеру, мы видим, как этот метод можно вызвать для инициализации внутреннего атрибута, который зависит от ранее заданных атрибутов.
@dataclass
class Person:
first_name: str = "Ahmed"
last_name: str = "Besbes"
age: int = 30
job: str = "Data Scientist"
full_name: str = field(init=False, repr=True)
def __post_init__(self):
self.full_name = self.first_name + " " + self.last_name
ahmed = Person()
print(ahmed)
# Person(first_name='Ahmed', last_name='Besbes', age=30, job='Data Scientist', full_name='Ahmed Besbes')
ahmed.full_name
#'Ahmed Besbes'
Обратите внимание, что аргументу repr
внутри функции field
присвоено значение True
, чтобы он был виден при выводе объекта.
В предыдущем примере мы не смогли установить для этого аргумента значение True
,
поскольку атрибут full_name
еще не был создан.
9 – Сравнение объектов и их сортировка
Одной из полезных механик, которую необходимо реализовать при работе с объектами, содержащими данные, является возможность сравнивать их и сортировать в любом желаемом порядке.
По умолчанию dataclasses
реализуют __eq__
.
Чтобы работали другие виды сравнения
(lt (меньше), le (меньше или равно), gt (больше) и ge (больше или равно)),
мы должны установить аргумент order
в значение True
в декораторе @dataclass
.
@dataclasses(order=True)
Реализация этих методов подразумевает следующее: берется определенное поле и сравнивается с другими, в том порядке, в котором они определены, до тех пор, пока не будет найдено неравное значение.
Давайте вернемся к классу Person
.
Допустим, мы хотим сравнить экземпляры этого класса по атрибуту возраста
(что имеет смысл, не так ли?).
Для этого нам нужно будет добавить поле, которое мы назовем sort_index
,
и установим его значение равным значению age
.
И мы сделаем это, вызвав метод __post_init__
,
который мы видели в предыдущем примере.
from dataclasses import dataclass, field
@dataclass(order=True)
class Person:
first_name: str = "Ahmed"
last_name: str = "Besbes"
age: int = 30
job: str = "Data Scientist"
sort_index: int = field(init=False, repr=False)
def __post_init__(self):
self.sort_index = self.age
p1 = Person(age=30)
p2 = Person(age=20)
print(p1 > p2)
# True
Теперь экземпляры класса Person
можно отсортировать по возрасту.
Подведем итог
Dataclasses
предоставляют множество механик,
которые позволяют легко работать с классами – контейнерами данных.
В частности, этот модуль помогает:
- Писать меньше шаблонного кода;
- Представлять объекты в удобочитаемом формате;
- Реализовывать собственный порядок сравнения;
- Предоставлять быстрый доступ к атрибутам и проверять их;
- Использовать специальные методы, такие как post_init для выполнения инициализации атрибутов, которые зависят от значений других атрибутов;
- Определять внутренние поля и т.д.
Источники:
Пока я изучал dataclasses
, я просмотрел множество ресурсов
(статьи в блогах, видео на YouTube, PEP, официальную документацию Python).
Вот мой личный список самых интересных постов и видео, которые я нашел: