Dataclass в Python: поля, default_factory и проверка данных

Dataclass в Python: поля, default_factory и проверка данных
Содержание статьи

Dataclass в Python помогает описать объект с данными, не писать вручную конструктор, строковое представление и сравнение полей. Но аннотации типов сами по себе не проверяют значения во время выполнения. Для собственного правила, например запрета пустого названия, нужна явная проверка.

В разборе Mentoring.digital соберём модель задачи. Пример рассчитан на Python 3.10 и новее и использует стандартную библиотеку.

Опишите поля и правила модели

У задачи будут название, число попыток и метки. Поле без значения по умолчанию ставим перед полями с такими значениями. Для списка используем фабрику, которая создаёт новый объект при каждом вызове конструктора.

from dataclasses import dataclass, field

@dataclass
class Task:
    title: str
    attempts: int = 0
    labels: list[str] = field(default_factory=list)

    def __post_init__(self):
        if not isinstance(self.title, str):
            raise TypeError("title должен быть строкой")
        self.title = self.title.strip()
        if not self.title:
            raise ValueError("Нужно название задачи")
        if type(self.attempts) is not int:
            raise TypeError("attempts должен быть целым числом")
        if self.attempts < 0:
            raise ValueError("attempts не может быть отрицательным")
        if not isinstance(self.labels, list):
            raise TypeError("labels должен быть списком")
        if any(not isinstance(x, str) for x in self.labels):
            raise TypeError("Каждая метка должна быть строкой")
        self.labels = self.labels.copy()

task = Task("  Проверить отчёт  ")
print(task.title)

Вызов автоматически созданного __init__ запускает __post_init__. Здесь нормализуется название и проверяются данные. Проверка attempts через type исключает также True и False: в Python bool является подклассом int, но для счётчика такое значение в нашем контракте не подходит.

Почему default_factory, а не общий список

Запись field(default_factory=list) передаёт саму функцию list, а не результат list(). У разных задач появляются разные списки. Дополнительная copy в примере отделяет модель от списка, который вызывающий код передал явно. Это поверхностное копирование; для списка строк его достаточно.

first = Task("Отчёт")
second = Task("Отчёт")
assert first == second
first.labels.append("срочно")
assert second.labels == []
assert first != second

source = ["работа"]
third = Task("Проверка", labels=source)
source.append("новая")
assert third.labels == ["работа"]

Не задавайте labels = [] прямо в объявлении поля. Dataclass отклоняет такие изменяемые значения по умолчанию. И не считайте фабрику универсальной защитой от общих объектов: если в неё поместить функцию, возвращающую один внешний список, он снова окажется общим.

Что dataclass не делает за вас

Сравнение по умолчанию учитывает поля и требует одинакового типа объектов. После изменения labels меняется и результат сравнения. Параметр frozen=True запрещает обычное присваивание полям, но не делает вложенные списки неизменяемыми. Для неизменяемых меток лучше отдельно продумать tuple и правила построения объекта.

Наш __post_init__ проверяет создание объекта, а не каждую последующую запись. Присваивание task.attempts = -1 обойдёт эту проверку. Если инвариант должен сохраняться всегда, ограничьте изменения методами, свойствами или другой моделью. Не используйте dataclass как замену проверке входного JSON.

Как проверить свою модель

  • Создайте две задачи и измените список только у одной.
  • Передайте пустое название, отрицательное число и строку вместо attempts.
  • Проверьте поведение при передаче внешнего списка.
  • Решите, допускает ли модель изменения после создания.

Такие упражнения можно включить в курс Python с индивидуальной практикой. Если модель уже используется в проекте, обсудите границы проверки и изменяемость с наставником по Python.

Похожие статьи по данной теме