dataclass и Enum: декларативные классы и перечисления

Писать __init__, __repr__, __eq__ (те самые dunder-методы) для каждого класса-контейнера данных - boilerplate. С Python 3.7 для этого есть @dataclass. А для перечислений типобезопасных констант - модуль enum. Оба добавлены в стандартную библиотеку и широко используются в современном коде.

Зачем dataclass

Без dataclass:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y

    def __hash__(self):
        return hash((self.x, self.y))

С dataclass:

from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: float
    y: float

Эквивалентно! Генерирует __init__, __repr__, __eq__, и (с frozen=True) __hash__.

Базовое использование

from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int
    email: str = "unknown@example.com"   # default

u1 = User(name="Alice", age=30)
u2 = User(name="Bob", age=25, email="bob@example.com")

print(u1)         # User(name='Alice', age=30, email='unknown@example.com')
print(u1 == User(name="Alice", age=30))   # True

Поля декларируются как аннотации типов классу. Типы могут быть любыми (хотя не проверяются в runtime - это hint для IDE и mypy).

Опции декоратора

@dataclass(
    init=True,           # генерировать __init__
    repr=True,           # генерировать __repr__
    eq=True,             # генерировать __eq__
    order=False,         # генерировать __lt__/__le__/__gt__/__ge__
    unsafe_hash=False,   # генерировать __hash__ даже для mutable
    frozen=False,        # сделать immutable
    match_args=True,     # поддержка match-case (Py 3.10+)
    kw_only=False,       # все поля keyword-only
    slots=False,         # __slots__ (Py 3.10+)
)
class Config:
    ...

Самые полезные:

  • frozen=True - immutable, добавляет hash, нельзя менять поля после создания
  • order=True - добавляет операторы сравнения для сортировки
  • slots=True - использует slots для экономии памяти
  • kw_only=True - все поля только по имени (для безопасности)

frozen - immutable dataclass

@dataclass(frozen=True)
class Coordinate:
    lat: float
    lng: float

c = Coordinate(55.75, 37.62)
c.lat = 56.0   # FrozenInstanceError - нельзя менять

frozen dataclasses hashable, безопасны для использования как ключи dict, не подвержены случайным мутациям. Часто идеальны для DTO.

Default factory

Для mutable defaults (списков, dict) нельзя использовать прямое значение:

@dataclass
class Order:
    items: list = []   # ОШИБКА при импорте

Используй field(default_factory=...):

from dataclasses import dataclass, field

@dataclass
class Order:
    items: list = field(default_factory=list)
    tags: set = field(default_factory=set)
    metadata: dict = field(default_factory=dict)

o1 = Order()
o2 = Order()
o1.items.append("apple")
print(o2.items)   # [] - НЕ shared, разные объекты

field(default_factory=...) вызывает функцию для каждого инстанса - решает проблему mutable defaults.

post_init - дополнительная инициализация

@dataclass
class User:
    name: str
    email: str

    def __post_init__(self):
        if "@" not in self.email:
            raise ValueError(f"Invalid email: {self.email}")
        self.normalized_name = self.name.strip().lower()

u = User("Alice", "alice@example.com")
print(u.normalized_name)   # alice

User("Bob", "invalid")     # ValueError

__post_init__ вызывается в конце сгенерированного __init__. Полезно для валидации и derived-полей.

InitVar - параметр только для init

from dataclasses import dataclass, field, InitVar

@dataclass
class Token:
    user_id: int
    expires_in: InitVar[int] = 3600
    expires_at: float = field(init=False)

    def __post_init__(self, expires_in):
        import time
        self.expires_at = time.time() + expires_in

t = Token(user_id=1, expires_in=60)
print(t.expires_at)
# t.expires_in   # AttributeError - InitVar не сохраняется как поле

InitVar - параметр, переданный в __init__, но не сохранённый как поле инстанса. Полезен для параметров с derived-полями.

Сравнение и сортировка

@dataclass(order=True)
class Version:
    major: int
    minor: int
    patch: int

v1 = Version(1, 0, 0)
v2 = Version(1, 1, 0)
v3 = Version(2, 0, 0)

print(sorted([v3, v1, v2]))
# [Version(1, 0, 0), Version(1, 1, 0), Version(2, 0, 0)]

order=True сравнивает по кортежу из всех полей лексикографически.

Иерархия dataclass

@dataclass
class Animal:
    name: str
    age: int

@dataclass
class Dog(Animal):
    breed: str

d = Dog("Rex", 3, "Labrador")   # все поля родителя + свои
print(d)   # Dog(name='Rex', age=3, breed='Labrador')

Поля родителя идут первыми. Аккуратно с default-значениями в родителе и без default в подклассе - могут возникнуть проблемы с порядком аргументов.

asdict и astuple

from dataclasses import asdict, astuple

@dataclass
class User:
    name: str
    age: int

u = User("Alice", 30)
print(asdict(u))    # {'name': 'Alice', 'age': 30}
print(astuple(u))   # ('Alice', 30)

asdict рекурсивно конвертирует в dict (включая вложенные dataclasses, списки, кортежи). Полезно для сериализации в JSON.

Enum - типизированные перечисления

Без Enum константы обычно через переменные:

RED = 1
GREEN = 2
BLUE = 3

def set_color(color):
    if color == RED:
        ...

Проблемы: можно случайно передать 4 вместо константы, нет имени при отладке, легко конфликтовать.

С Enum:

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

print(Color.RED)              # Color.RED
print(Color.RED.value)        # 1
print(Color.RED.name)         # 'RED'
print(Color(1))               # Color.RED - по значению
print(Color["RED"])           # Color.RED - по имени

# Итерация
for color in Color:
    print(color)

Enum обеспечивает:

  • Уникальные значения (можно настроить через @unique)
  • Type-safety: Color.RED is Color.RED (одиночные синглетоны)
  • Читаемые repr
  • Защита от случайных значений

IntEnum, StrEnum, IntFlag

Подклассы для специфичных случаев:

from enum import IntEnum, StrEnum

class Status(IntEnum):
    ACTIVE = 1
    INACTIVE = 0

# Можно сравнивать с int
Status.ACTIVE == 1   # True

class LogLevel(StrEnum):    # Py 3.11+
    DEBUG = "debug"
    INFO = "info"
    WARNING = "warning"

LogLevel.DEBUG == "debug"   # True

IntEnum/StrEnum полезны при сериализации (значение само сохраняется), legacy-кодом, JSON-совместимостью.

auto() - автонумерация

from enum import Enum, auto

class Status(Enum):
    PENDING = auto()    # 1
    APPROVED = auto()   # 2
    REJECTED = auto()   # 3

auto() присваивает следующий int. Удобно когда значения не важны, только идентификаторы.

Методы и members

class HttpMethod(Enum):
    GET = "GET"
    POST = "POST"
    PUT = "PUT"
    DELETE = "DELETE"

    def is_safe(self):
        return self in (HttpMethod.GET,)

    def is_idempotent(self):
        return self in (HttpMethod.GET, HttpMethod.PUT, HttpMethod.DELETE)

print(HttpMethod.GET.is_safe())          # True
print(HttpMethod.POST.is_idempotent())   # False

# Доступ ко всем членам
print(HttpMethod.__members__)
# {'GET': <HttpMethod.GET: 'GET'>, 'POST': ...}

В Enum можно добавлять методы. Это даёт типобезопасные «функции для значения».

Уникальность

По умолчанию Enum разрешает aliases (несколько имён для одного значения):

class Color(Enum):
    RED = 1
    SCARLET = 1   # alias для RED
    BLUE = 2

print(Color.SCARLET is Color.RED)   # True - alias
list(Color)                          # [RED, BLUE] - alias скрыт в итерации

Чтобы запретить - @unique:

from enum import Enum, unique

@unique
class Color(Enum):
    RED = 1
    SCARLET = 1   # ValueError - дубликат

Сравнение

КонцепцияИспользование
@dataclassКонтейнер данных с автогенерацией init/repr/eq
@dataclass(frozen=True)Immutable DTO, hashable
@dataclass(slots=True)Память-экономный data-объект
NamedTupleЛёгкий immutable, поддерживает распаковку
EnumПеречисление с уникальными значениями
IntEnum/StrEnumEnum с auto-совместимостью с int/str

В новом коде часто: @dataclass для mutable объектов, @dataclass(frozen=True) для immutable DTO, Enum для перечислений.

Pydantic - расширение для валидации

Pydantic это сторонняя библиотека (используется в FastAPI), близкая по идее к dataclass, но с runtime валидацией:

from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int
    email: str

u = User(name="Alice", age="30")   # age автоматически приводится к int
u = User(name="Alice", age="abc")  # ValidationError

Pydantic подробно в Модуле 10. Для backend-проектов часто используется вместо обычного dataclass.

Распространённые ошибки

1. Mutable default без factory

@dataclass
class Order:
    items: list = []   # ValueError при определении класса

Используй field(default_factory=list).

2. Default value у поля родителя

@dataclass
class Animal:
    name: str = "Unknown"

@dataclass
class Dog(Animal):
    breed: str   # TypeError - non-default после default

Поля без default должны идти перед полями с default. Используй kw_only=True или переставь поля.

3. Enum значения должны быть hashable

class Bad(Enum):
    X = [1, 2]   # TypeError - list unhashable

Используй tuple или другие immutable значения.

Сравнение с Go и PHP

В Go нет dataclass - используются обычные struct:

type User struct {
    Name string
    Age  int
}

u := User{Name: "Alice", Age: 30}
fmt.Println(u)   // {Alice 30}

Сравнение и сериализация делаются вручную или через библиотеки.

В PHP 8 появились readonly properties и promoted constructor параметры:

class User {
    public function __construct(
        public readonly string $name,
        public readonly int $age,
    ) {}
}

Похоже на dataclass, но без автогенерации __toString, equality и других методов.

Python dataclass предоставляет наиболее богатый автогенерируемый функционал из этих языков.

Мини-задание

  1. Базовый dataclass:
from dataclasses import dataclass, field
from datetime import datetime

@dataclass
class Article:
    title: str
    author: str
    content: str = ""
    tags: list = field(default_factory=list)
    created_at: datetime = field(default_factory=datetime.now)

a = Article(title="Python OOP", author="Alice")
print(a)
  1. Frozen для DTO:
@dataclass(frozen=True, order=True)
class SemVer:
    major: int
    minor: int
    patch: int

    def __str__(self):
        return f"{self.major}.{self.minor}.{self.patch}"

versions = [SemVer(1, 0, 0), SemVer(2, 0, 0), SemVer(1, 5, 0)]
for v in sorted(versions):
    print(v)
# 1.0.0, 1.5.0, 2.0.0

# В set
unique = {SemVer(1, 0, 0), SemVer(1, 0, 0), SemVer(2, 0, 0)}
print(len(unique))   # 2 - hashable, дедупликация
  1. Enum с методами:
from enum import Enum

class TrafficLight(Enum):
    RED = "red"
    YELLOW = "yellow"
    GREEN = "green"

    def next(self):
        order = [TrafficLight.GREEN, TrafficLight.YELLOW, TrafficLight.RED]
        idx = order.index(self)
        return order[(idx + 1) % len(order)]

light = TrafficLight.GREEN
for _ in range(5):
    print(light)
    light = light.next()

Что дальше

Освоили dataclass и Enum. В следующем уроке - ABC и Protocol: абстрактные классы для явного наследования и Protocol для duck typing с типизацией. Это завершит Модуль 6. Если нужна не просто структура данных, а ещё и валидация значений на входе - смотри урок про Pydantic: там тот же декларативный стиль доведён до полноценных моделей.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.