dataclass в Python: классы данных без шаблонного кода
Декоратор @dataclass строит служебные методы класса по объявленным полям: обычно __init__, читаемый __repr__ и сравнение __eq__. Он подходит объектам, чья основная роль — хранить именованные данные. Это остаётся обычным классом Python: можно добавлять методы, свойства, проверки после инициализации и выбирать параметры генерации.
Поля определяют модель
Аннотация в теле dataclass объявляет поле экземпляра. Поля без значения по умолчанию должны идти до полей со значением по умолчанию. Сгенерированный конструктор принимает их в этом порядке, хотя для читаемости вызовов часто используют именованные аргументы.
Аннотации не проверяют типы во время выполнения автоматически. Если цена должна быть неотрицательной, инвариант удобно проверять в __post_init__, который вызывается после сгенерированного __init__. Вычисляемое значение необязательно хранить полем: простое свойство исключает рассинхронизацию с исходными данными.
Рабочий пример: строка заказа
from dataclasses import dataclass
@dataclass(frozen=True)
class OrderLine:
product: str
unit_price: int
quantity: int = 1
def __post_init__(self):
if self.unit_price < 0 or self.quantity <= 0:
raise ValueError('invalid order line')
@property
def total(self):
return self.unit_price * self.quantity
line = OrderLine(product='Клавиатура', unit_price=3500, quantity=2)
same = OrderLine('Клавиатура', 3500, 2)
print(line)
print(line.total)
print(line == same)Ожидаемый результат:
OrderLine(product='Клавиатура', unit_price=3500, quantity=2)
7000
Truefrozen=True запрещает обычное присваивание полям после создания и делает намерение неизменяемости явным. Это не превращает вложенные изменяемые объекты в неизменяемые, поэтому состав полей всё равно нужно проектировать осознанно.
Изменяемый default и порядок полей
Для поля-коллекции нельзя писать tags: list[str] = []: один список оказался бы общим для экземпляров, а dataclass предотвращает распространённый случай сообщением ValueError о mutable default. Правильная запись — tags: list[str] = field(default_factory=list). Фабрика вызывается для каждого нового объекта и создаёт независимый список.
Если конструктор сообщает TypeError: non-default argument ... follows default argument, проверьте порядок полей во всём классе, включая унаследованные поля: обязательные должны предшествовать необязательным. При неожиданном равенстве изучите, какие поля участвуют в сравнении, и не исключено ли важное поле через compare=False. Сгенерированные методы удобны, но их правила определяются параметрами и описанием полей.
Тренировка: dataclass с собственной коллекцией
Создайте @dataclass для StudyGroup с обязательным name: str и students: list[str], получаемым через field(default_factory=list). Добавьте метод add_student, который не допускает пустое имя и повтор. Создайте две группы, измените только первую и подтвердите, что список второй остался пустым.
Самостоятельная проверка: временно попробуйте заменить фабрику на пустой список и зафиксируйте сообщение Python, затем верните корректный вариант. Сравните две группы с одинаковыми полями, измените одну и предскажите результат ==. Добавьте __post_init__, запрещающий пустое название группы.
Поля не отменяют проектирование модели
@dataclass устраняет повторяющийся код вокруг полей, но не проектирует модель вместо разработчика. Аннотации задают состав полей, __post_init__ поддерживает инварианты, а default_factory создаёт отдельные изменяемые значения. Параметры frozen, order и настройки сравнения нужно включать только при ясной предметной семантике.
Возможности и границы dataclass
Dataclass — это просто словарь?
Нет. Это класс с именованными атрибутами, методами и обычным механизмом типов Python. Декоратор лишь генерирует часть стандартных методов на основе полей.
Делает ли frozen объект полностью неизменяемым?
Он блокирует обычное переназначение полей экземпляра. Если поле ссылается на изменяемый список или словарь, содержимое такого объекта может по-прежнему меняться.
Когда обычный класс лучше dataclass?
Когда объект в основном скрывает сложное поведение, имеет нестандартный жизненный цикл или его публичный контракт не должен совпадать с набором хранимых полей. В таких случаях явные методы могут выразить модель точнее.