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





