Конструкции 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
– пустой словарь.