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/StrEnum | Enum с 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 предоставляет наиболее богатый автогенерируемый функционал из этих языков.
Мини-задание
- Базовый 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)
- 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, дедупликация
- 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: там тот же декларативный стиль доведён до полноценных моделей.