dataclass — декоратор, который дописывает в класс методы, обычно набираемые руками: конструктор, читаемое представление и сравнение по значению. Пишешь только имена полей с типами, остальное появляется само. Код выполнен на CPython 3.14.5.
Поля описываешь ты. Служебный код — dataclass
@dataclass сам создаст конструктор, понятную печать и сравнение объектов по значениям.Что делает декоратор
Класс, хранящий три значения, без него выглядит так:
class OrderPlain:
def __init__(self, id, amount, status="new"):
self.id = id
self.amount = amount
self.status = status
o = OrderPlain(10, 900)
print(repr(o).startswith("<__main__.OrderPlain object at"))
print(o == OrderPlain(10, 900))
True
False
Две проблемы сразу. Печать объекта даёт <__main__.OrderPlain object at 0x7f...> с адресом в памяти, который меняется от запуска к запуску и ничего не говорит о содержимом. Сравнение двух одинаковых заказов даёт False, потому что по умолчанию Python сравнивает объекты по тождеству, а не по значениям.
Тот же класс с декоратором:
from dataclasses import dataclass
@dataclass
class Order:
id: int
amount: int
status: str = "new"
o = Order(10, 900)
print(o)
print(o == Order(10, 900))
Order(id=10, amount=900, status='new')
True
Конструктор не написан, а работает. Печать показывает поля. Сравнение идёт по значениям. Проверить, что именно сгенерировалось, можно прямо у класса:
from dataclasses import dataclass, fields
@dataclass
class Order:
id: int
amount: int
status: str = "new"
print([m for m in ("__init__", "__repr__", "__eq__") if m in Order.__dict__])
print([f.name for f in fields(Order)])
['__init__', '__repr__', '__eq__']
['id', 'amount', 'status']
Механика та же, что у любого декоратора: он получает класс, дописывает в него методы и возвращает обратно.
Аннотация обязательна, но не проверяется
Поле появляется только у имени с аннотацией типа. Без неё это обычный атрибут класса:
from dataclasses import dataclass, fields
@dataclass
class Half:
a: int
b = 5
print([f.name for f in fields(Half)], Half(1).b)
['a'] 5
b не попал в поля, значит его нет ни в конструкторе, ни в сравнении. Пропущенное двоеточие с типом — тихая ошибка: класс соберётся, а поле исчезнет.
При этом сама аннотация ни на что не влияет во время работы. Python её не проверяет:
from dataclasses import dataclass
@dataclass
class Order:
id: int
amount: int
status: str = "new"
print(Order("десять", "много"))
Order(id='десять', amount='много', status='new')
Строка вместо числа прошла без единого возражения. Аннотации нужны читателю и статическим анализаторам; проверку типов во время работы делают отдельные библиотеки.
Порядок полей и значения по умолчанию
Поле без значения не может стоять после поля со значением, иначе конструктор нельзя было бы вызвать позиционно:
from dataclasses import dataclass
try:
@dataclass
class Bad:
status: str = "new"
id: int
except TypeError as exc:
print(f"TypeError: {exc}")
TypeError: non-default argument 'id' follows default argument 'status'
Обойти ограничение, не переставляя поля, помогает kw_only: тогда все аргументы становятся именованными и порядок перестаёт иметь значение.
from dataclasses import dataclass
@dataclass(kw_only=True)
class Conf:
host: str
port: int = 5432
print(Conf(host="localhost"))
try:
Conf("localhost")
except TypeError as exc:
print(f"TypeError: {exc}")
Conf(host='localhost', port=5432)
TypeError: Conf.__init__() takes 1 positional argument but 2 were given
Почему список нельзя записать по умолчанию
Попытка дать полю изменяемое значение по умолчанию отвергается сразу:
from dataclasses import dataclass
try:
@dataclass
class BadList:
items: list = []
except ValueError as exc:
print(f"ValueError: {exc}")
ValueError: mutable default <class 'list'> for field items is not allowed: use default_factory
Это редкий случай, когда язык ловит ошибку за тебя. У обычной функции def f(items=[]) тот же дефект проходит молча и приводит к общему списку на все вызовы, а dataclass отказывается собираться и прямо называет решение.
default_factory вызывает фабрику при создании каждого объекта:
from dataclasses import dataclass, field
@dataclass
class Cart:
items: list = field(default_factory=list)
a, b = Cart(), Cart()
a.items.append("мышь")
print(a, b)
Cart(items=['мышь']) Cart(items=[])
Списки разные, добавление в один не задело другой. Почему это вообще проблема и откуда она растёт, разобрано в статье про изменяемые и неизменяемые типы.
frozen: неизменяемый объект и ключ словаря
Аргумент frozen=True запрещает присваивание полям после создания:
from dataclasses import dataclass, FrozenInstanceError
@dataclass(frozen=True)
class Point:
x: int
y: int
p = Point(1, 2)
try:
p.x = 5
except FrozenInstanceError as exc:
print(f"FrozenInstanceError: {exc}")
print(Point(1, 2) in {p})
FrozenInstanceError: cannot assign to field 'x'
True
Вторая строка важнее первой. Этот замороженный объект попал в множество и нашёлся там по значению: его поля хешируемы, поэтому он годится и в ключи словаря. Одного frozen=True недостаточно: поле-список, участвующее в хеше, вызовет TypeError. Заморозка запрещает переприсваивать поля, но не менять содержимое вложенного списка. Обычный dataclass с настройками по умолчанию не хешируется:
from dataclasses import dataclass
@dataclass
class Order:
id: int
amount: int
try:
{Order(10, 900)}
except TypeError as exc:
print(f"TypeError: {exc}")
TypeError: cannot use 'Order' as a set element (unhashable type: 'Order')
При eq=True и frozen=False декоратор отключает __hash__, поскольку поля, от которых зависит равенство, могут измениться. Это не общий запрет на хеширование изменяемых объектов: обычные пользовательские классы по умолчанию сравниваются и хешируются по идентичности. Чтобы получить объект с изменённым полем, не меняя исходный, используют replace:
from dataclasses import dataclass, replace
@dataclass
class Order:
id: int
amount: int
o = Order(10, 900)
print(replace(o, amount=1200), o)
Order(id=10, amount=1200) Order(id=10, amount=900)
order: сортировка объектов
С order=True появляются операторы сравнения, и объекты можно сортировать без всякого key:
from dataclasses import dataclass
@dataclass(order=True)
class Score:
points: int
name: str
print(sorted([Score(3, "Аня"), Score(1, "Борис"), Score(3, "Вера")]))
[Score(points=1, name='Борис'), Score(points=3, name='Аня'), Score(points=3, name='Вера')]
Сравнение идёт по всем полям слева направо, как у кортежа: сначала очки, при равенстве имя. Отсюда важное следствие: порядок объявления полей задаёт порядок сортировки, и поле, попавшее в класс случайно, будет участвовать в сравнении.
Исключить поле из сравнения можно точечно:
from dataclasses import dataclass, field
@dataclass(order=True)
class Row:
key: int
note: str = field(compare=False, default="")
print(Row(1, "первый") == Row(1, "второй"))
True
Тот же приём у repr: field(repr=False) убирает поле из печати, что удобно для паролей и токенов.
from dataclasses import dataclass, field
@dataclass
class User:
login: str
password: str = field(repr=False, default="")
print(User("anya", "secret"))
User(login='anya')
Вычисляемое поле и превращение в словарь
Значение, зависящее от других полей, считают в __post_init__, а само поле помечают init=False, чтобы оно не просилось в конструктор:
from dataclasses import dataclass, field
@dataclass
class Rect:
w: int
h: int
area: int = field(init=False)
def __post_init__(self):
self.area = self.w * self.h
print(Rect(3, 4))
Rect(w=3, h=4, area=12)
Для отдачи наружу объект разворачивают в словарь или кортеж. Вложенные объекты разворачиваются тоже:
from dataclasses import dataclass, asdict
@dataclass
class Order:
id: int
amount: int
@dataclass
class Client:
name: str
last: Order
print(asdict(Client("Аня", Order(10, 900))))
{'name': 'Аня', 'last': {'id': 10, 'amount': 900}}
Это готовый мост к JSON и к любому коду, который ждёт обычные словари.
Если self, __init__ и __repr__ пока выглядят загадочно, начни с разбора обычных классов: dataclass пишет их за тебя, но понимать, что именно он пишет, всё равно нужно.
Когда dataclass не нужен
Данные пришли извне и уходят наружу без обработки. Разобранный JSON — это уже словарь, и оборачивать его в класс ради одного прохода незачем.
Нужна проверка типов на входе. Аннотации в dataclass декоративные. Если данные приходят от пользователя и их надо валидировать, берут библиотеку с проверкой, а не пишут её сами в __post_init__.
Нужен только неизменяемый набор значений без методов. NamedTuple короче, распаковывается как кортеж и хешируется, если хешируемы все его элементы.
Класс на самом деле про поведение, а не про данные. Если полей два, а методов десять, это обычный класс, и декоратор ничего не улучшит.
Частые вопросы
Чем dataclass отличается от обычного класса
Ничем по устройству: это тот же класс, которому декоратор дописал __init__, __repr__ и __eq__. Всё остальное, включая методы и наследование, работает как обычно.
Как сделать поле со списком по умолчанию
field(default_factory=list). Просто написать = [] язык запрещает, чтобы список не оказался общим для всех объектов.
Как запретить менять объект после создания
@dataclass(frozen=True) запрещает присваивание полям, но не изменение вложенных объектов. При стандартном eq=True декоратор также создаёт __hash__; объект сможет быть ключом словаря, если все участвующие в хеше поля хешируемы.
Проверяет ли dataclass типы
Нет. Order("десять", "много") соберётся без ошибки. Аннотации нужны читателю и статическим анализаторам.
Чем dataclass отличается от NamedTuple
NamedTuple ведёт себя как кортеж: его поля нельзя переприсвоить, он распаковывается и индексируется. Хеширование возможно, если все элементы хешируемы. dataclass по умолчанию изменяем, допускает методы и настраивается аргументами декоратора.
Где потренироваться
В пути «Python: ООП» на Koddo классы данных идут вместе с моделями предметной области: заказ, клиент, позиция корзины. Рядом стоят декораторы, на которых dataclass и построен, и паттерны проектирования, где он служит готовым строителем.