1. Введение в dataclass
dataclasses — модуль стандартной библиотеки Python, добавленный в версии 3.7 (PEP 557). Предоставляет декоратор @dataclass, который автоматически генерирует специальные методы (__init__, __repr__, __eq__, __hash__ и другие) для классов, предназначенных в основном для хранения данных.
Основная идея: вы описываете данные (поля с типами), а Python сам генерирует «скучной» код, который иначе пришлось бы писать вручную.
Зачем нужен dataclass
- Меньше кода: не нужно писать
__init__,__repr__,__eq__вручную - Типизация: поля аннотированы типами — лучше читаемость, поддержка IDE и mypy
- Неизменяемость: параметр
frozen=Trueсоздаёт иммутабельный объект - Сравнение: автоматическая сортировка и сравнение по полям
- Сериализация: легкое преобразование в dict/tuple для JSON, баз данных
- Валидация:
__post_init__для проверки значений после инициализации - Производительность:
slots=True(Python 3.10+) уменьшает память и ускоряет доступ - InitVar: временные переменные, передаваемые в init, но не сохраняемые как поля
- Интроспекция:
__dataclass_fields__для программного анализа структуры
2. Базовая структура
Минимальный пример
from dataclasses import dataclass
@dataclass
class User:
name: str
email: str
age: int = 0
user = User(name="Alice", email="alice@example.com", age=30)
print(user) # User(name='Alice', email='alice@example.com', age=30)
Этот код автоматически генерирует:
__init__(self, name: str, email: str, age: int = 0)__repr__(self)— читаемое представление__eq__(self, other)— сравнение по всем полям
Что генерирует dataclass
| Метод | Условие | Описание |
|---|---|---|
__init__ | Всегда (кроме init=False) | Конструктор с полями в порядке объявления |
__repr__ | Всегда (кроме repr=False) | ClassName(field=value, ...) |
__eq__ | Всегда (кроме eq=False) | Сравнение по всем полям |
__hash__ | Зависит от frozen/eq | Хэш: если frozen=True или unsafe_hash=True |
__lt__, __le__, __gt__, __ge__ | order=True | Операторы для сортировки |
__delattr__, __setattr__ | frozen=True | Запрет на изменение полей |
__match_args__ | match_args=True (3.10+) | Поддержка pattern matching |
Правило генерации __hash__
# eq=True + frozen=True → __hash__ генерируется (хэшируемый)
# eq=True + frozen=False → __hash__ = None (нехэшируемый)
# eq=False → __hash__ наследуется от object
# unsafe_hash=True → __hash__ принудительно (даже для мутабельного)
@dataclass(frozen=True)
class Hashable:
x: int
print(hash(Hashable(1))) # OK
@dataclass
class NotHashable:
x: int
# hash(NotHashable(1)) # TypeError: unhashable type
3. Параметры dataclass
| Параметр | По умолч. | Описание | Версия |
|---|---|---|---|
init | True | Генерировать __init__ | 3.7 |
repr | True | Генерировать __repr__ | 3.7 |
eq | True | Генерировать __eq__ | 3.7 |
order | False | Генерировать __lt__, __le__, __gt__, __ge__ | 3.7 |
frozen | False | Сделать экземпляры неизменяемыми | 3.7 |
unsafe_hash | False | Принудительно генерировать __hash__ | 3.7 |
match_args | True | Генерировать __match_args__ для pattern matching | 3.10 |
kw_only | False | Все поля — keyword-only | 3.10 |
slots | False | Генерировать __slots__ | 3.10 |
weakref_slot | False | Добавить __weakref__ в slots | 3.11 |
@dataclass(order=True, frozen=True, kw_only=True)
class Point:
x: float
y: float
p1 = Point(x=1.0, y=2.0)
p2 = Point(x=1.0, y=3.0)
print(p1 < p2) # True (сравнение по x, потом по y)
# Все параметры False:
@dataclass(init=False, repr=False, eq=False, order=False)
class Raw:
"""Полностью ручной класс — ничего не генерируется"""
name: str
def __init__(self, name: str):
self.name = name.upper()
4. Поля (field)
Функция field() позволяет задать детальные настройки для каждого поля:
from dataclasses import dataclass, field
from typing import List
@dataclass
class Order:
id: int
items: List[str] = field(default_factory=list)
discount: float = field(default=0.0, repr=False)
created_at: str = field(compare=False)
metadata: dict = field(default_factory=dict, hash=False, compare=False)
Параметры field
| Параметр | Назначение |
|---|---|
default | Значение по умолчанию |
default_factory | Фабрика для мутабельных дефолтов (list, dict, set, lambda) |
init | Включить поле в __init__ (по умолч. True) |
repr | Включить поле в __repr__ (по умолч. True) |
compare | Учитывать поле при сравнении (по умолч. True) |
hash | Учитывать поле при вычислении хэша (по умолч. None = как compare) |
metadata | Произвольные метаданные (dict) |
kw_only | Сделать поле keyword-only (3.10+) |
default_factory. items: List[str] = [] — ValueError!Метаданные (metadata)
@dataclass
class Product:
name: str = field(metadata={"description": "Название товара", "max_length": 255})
price: float = field(metadata={"unit": "USD", "min": 0.0, "currency": "USD"})
print(Product.__dataclass_fields__["name"].metadata["description"])
print(Product.__dataclass_fields__["price"].metadata)
# → {'unit': 'USD', 'min': 0.0, 'currency': 'USD'}
// Пример: генерация JSON Schema из metadata
def fields_to_json_schema(cls):
schema = {"type": "object", "properties": {}}
for name, f in cls.__dataclass_fields__.items():
schema["properties"][name] = f.metadata
return schema
print(fields_to_json_schema(Product))
# {'type': 'object', 'properties': {
# 'name': {'description': 'Название товара', 'max_length': 255},
# 'price': {'unit': 'USD', 'min': 0.0, 'currency': 'USD'}
# }}
field с кастомным default_factory
import secrets, uuid
from datetime import datetime, timedelta
@dataclass
class AuthSession:
user_id: int
token: str = field(default_factory=lambda: secrets.token_hex(32))
csrf_token: str = field(default_factory=lambda: secrets.token_urlsafe(32))
expires_at: datetime = field(
default_factory=lambda: datetime.now() + timedelta(hours=2)
)
id: uuid.UUID = field(default_factory=uuid.uuid4)
metadata: dict = field(default_factory=lambda: {"version": 1})
s = AuthSession(user_id=1)
print(s.token) # случайный 64-символьный hex
print(s.expires_at) # +2 часа
5. Типы полей
Стандартные типы
from dataclasses import dataclass, field
from typing import Optional, List, Dict, Set, Tuple, Union, Any, Literal
from decimal import Decimal
from datetime import datetime, date, time
from uuid import UUID
from pathlib import Path
from enum import Enum, auto
class Color(Enum):
RED = auto()
GREEN = auto()
BLUE = auto()
@dataclass
class AllTypes:
# Примитивы
id: int
name: str
active: bool
ratio: float
data: bytes
# Optional
nickname: Optional[str] = None
# Коллекции
tags: List[str] = field(default_factory=list)
scores: Dict[str, float] = field(default_factory=dict)
roles: Set[str] = field(default_factory=set)
matrix: List[List[int]] = field(default_factory=list)
# Специализированные
balance: Decimal = Decimal("0.00")
created_at: datetime = field(default_factory=datetime.now)
birth_date: date = field(default_factory=date.today)
lunch_time: time = time(12, 0)
uid: UUID = field(default_factory=UUID.uuid4)
path: Path = Path("/tmp")
color: Color = Color.RED
# Union / Any / Literal
payload: Any = None
status: Literal["pending", "active", "done"] = "pending"
metadata: Union[Dict[str, Any], None] = None
# Tuple (фиксированная длина)
coords: Tuple[float, float] = (0.0, 0.0)
config: Tuple[int, str, bool] = (0, "", False)
Вложенные dataclass
@dataclass
class Address:
city: str
street: str
zip_code: str
is_primary: bool = False
@dataclass
class Employee:
name: str
address: Address
department: str
addresses: List[Address] = field(default_factory=list)
metadata: Dict[str, Address] = field(default_factory=dict)
emp = Employee(
name="Bob",
address=Address(city="Москва", street="Тверская", zip_code="101000"),
department="IT",
addresses=[
Address(city="Москва", street="Тверская", zip_code="101000", is_primary=True),
Address(city="СПб", street="Невский", zip_code="190000"),
]
)
print(emp)
# Employee(name='Bob', address=Address(...), ...)
Рекурсивные dataclass (деревья)
from typing import Optional, List
@dataclass
class TreeNode:
value: int
left: Optional["TreeNode"] = None
right: Optional["TreeNode"] = None
def is_leaf(self) -> bool:
return self.left is None and self.right is None
tree = TreeNode(
value=1,
left=TreeNode(
value=2,
left=TreeNode(4),
right=TreeNode(5),
),
right=TreeNode(value=3),
)
@dataclass
class Comment:
id: int
text: str
replies: List["Comment"] = field(default_factory=list)
parent: Optional["Comment"] = None
def add_reply(self, text: str) -> "Comment":
reply = Comment(id=len(self.replies) + 1, text=text, parent=self)
self.replies.append(reply)
return reply
post = Comment(id=1, text="Great post!")
post.add_reply("Thanks!")
post.add_reply("I agree")
6. post_init — валидация
Метод __post_init__ выполняется сразу после __init__. Используется для валидации, вычисления полей, преобразования типов.
Валидация значений
@dataclass
class Person:
name: str
age: int
email: str = ""
def __post_init__(self):
if self.age < 0:
raise ValueError(f"Age must be positive: {self.age}")
if not self.name.strip():
raise ValueError("Name cannot be empty")
self.name = self.name.strip().title()
if self.email and "@" not in self.email:
raise ValueError(f"Invalid email: {self.email}")
p = Person(name=" alice ", age=30)
print(repr(p)) # Person(name='Alice', age=30, email='')
Вычисляемые поля
from datetime import datetime
import hashlib
@dataclass
class Article:
title: str
content: str
created_at: datetime = field(default_factory=datetime.now, init=False)
slug: str = field(init=False)
content_hash: str = field(init=False, repr=False)
def __post_init__(self):
self.slug = self.title.lower().replace(" ", "-")
import re
self.slug = re.sub(r"[^a-z0-9-]", "", self.slug)
self.content_hash = hashlib.md5(self.content.encode()).hexdigest()
a = Article(title="Hello World!", content="Some text")
print(a.slug) # "hello-world"
print(a.created_at) # 2026-06-20 12:34:56
print(a.content_hash) # md5 хэш контента
Преобразование типов в post_init
from datetime import datetime
from decimal import Decimal
from uuid import UUID
@dataclass
class ParsedEvent:
raw_id: str
raw_date: str
raw_amount: str
id: UUID = field(init=False)
date: datetime = field(init=False)
amount: Decimal = field(init=False)
def __post_init__(self):
self.id = UUID(self.raw_id)
self.date = datetime.fromisoformat(self.raw_date)
self.amount = Decimal(self.raw_amount)
event = ParsedEvent(
raw_id="550e8400-e29b-41d4-a716-446655440000",
raw_date="2026-06-20T12:00:00",
raw_amount="199.99",
)
print(event.id) # UUID(...)
print(event.amount) # Decimal('199.99')
Порядок вызова post_init при наследовании
@dataclass
class BaseConfig:
name: str
def __post_init__(self):
print(f"BaseConfig: name={self.name}")
self._validate()
def _validate(self):
if not self.name:
raise ValueError("Name required")
@dataclass
class ExtendedConfig(BaseConfig):
version: int = 1
def __post_init__(self):
super().__post_init__() # обязательно!
print(f"ExtendedConfig: version={self.version}")
if self.version < 0:
raise ValueError("Version must be positive")
c = ExtendedConfig(name="app", version=2)
# BaseConfig: name=app
# ExtendedConfig: version=2
7. Наследование
Простое наследование
@dataclass
class BaseUser:
id: int
name: str
created_at: datetime = field(default_factory=datetime.now, init=False)
@dataclass
class Admin(BaseUser):
level: int = 1
permissions: List[str] = field(default_factory=lambda: ["read"])
def grant(self, perm: str) -> None:
self.permissions.append(perm)
admin = Admin(id=1, name="Alice", level=5)
admin.grant("write")
print(admin)
# Admin(id=1, name='Alice', level=5, permissions=['read', 'write'],
# created_at=datetime(...))
Проблема порядка полей
__init__. Если в дочернем классе есть поля с умолчанием, они не могут стоять перед полями родителя — TypeError.@dataclass
class Base:
name: str # обязательное
# Ошибка!
# @dataclass
# class Child(Base):
# extra: str = "default"
# # TypeError: non-default argument follows default argument
# # (name идёт после extra в сгенерированном __init__)
# Решение 1: kw_only на уровне класса
@dataclass(kw_only=True)
class Animal:
name: str
species: str
@dataclass(kw_only=True)
class Dog(Animal):
breed: str = "unknown"
age: int = 0
Dog(name="Rex", species="Canine", breed="Husky") # OK
# Решение 2: дать умолчание полю родителя
@dataclass
class Base2:
name: str = "default"
@dataclass
class Child2(Base2):
extra: str = "value"
Child2("test", "extra") # OK
Множественное наследование
@dataclass
class Named:
name: str
@dataclass
class Versioned:
version: int = 1
@dataclass
class Timestamped:
created_at: datetime = field(default_factory=datetime.now, init=False)
@dataclass
class FullEntity(Named, Versioned, Timestamped):
id: int
e = FullEntity(name="test", id=1, version=2)
print(e.created_at) # datetime.now()
# MRO: FullEntity → Named → Versioned → Timestamped → object
# Поля: name, version, id, created_at (init=False)
8. Frozen dataclass
frozen=True делает экземпляры неизменяемыми. Попытка изменить поле — FrozenInstanceError.
Базовый пример
@dataclass(frozen=True)
class Config:
host: str
port: int
debug: bool = False
cfg = Config(host="localhost", port=8080)
# cfg.port = 9090 # FrozenInstanceError
# Frozen → хэшируемый → можно в set/dict keys
configs: set = {Config("a", 1), Config("b", 2), Config("a", 1)}
print(len(configs)) # 2
replace — копирование с изменениями
from dataclasses import replace
@dataclass(frozen=True)
class Point:
x: int
y: int
p1 = Point(1, 2)
p2 = replace(p1, x=10)
print(p1) # Point(x=1, y=2) — исходный не изменился
print(p2) # Point(x=10, y=2) — новый объект
// replace в цепочках:
p3 = replace(p2, y=20)
print(p3) # Point(x=10, y=20)
// replace с вычисляемым значением:
p4 = replace(p1, x=p1.x + 5)
print(p4) # Point(x=6, y=2)
Frozen + post_init
В __post_init__ frozen-класса нельзя присвоить поле напрямую. Используйте object.__setattr__:
@dataclass(frozen=True)
class ImmutablePerson:
name: str
age: int
age_group: str = field(init=False)
def __post_init__(self):
object.__setattr__(self, "age_group",
"child" if self.age < 18 else
"teen" if self.age < 21 else
"adult")
p = ImmutablePerson(name="Alice", age=25)
print(p.age_group) # "adult"
// Утилита для удобства:
def set_frozen(obj, **kwargs):
for k, v in kwargs.items():
object.__setattr__(obj, k, v)
@dataclass(frozen=True)
class FrozenConfig:
host: str
def __post_init__(self):
set_frozen(self, url=f"https://{self.host}")
print(FrozenConfig(host="example.com").url)
# https://example.com
Frozen как value object
@dataclass(frozen=True)
class Money:
amount: Decimal
currency: str = "USD"
def __add__(self, other: "Money") -> "Money":
if self.currency != other.currency:
raise ValueError("Currency mismatch")
return Money(self.amount + other.amount, self.currency)
def __mul__(self, factor: float) -> "Money":
return Money(self.amount * Decimal(str(factor)), self.currency)
def __repr__(self):
return f"{self.amount:.2f} {self.currency}"
price = Money(Decimal("99.99"))
tax = Money(Decimal("10.00"))
total = price + tax
bulk = price * 3
print(total) # 109.99 USD
print(bulk) # 299.97 USD
9. Сравнение и сортировка
Порядок сравнения
@dataclass(order=True)
class Task:
priority: int
created_at: str
title: str = field(compare=False)
tasks = [
Task(2, "2026-01-01", "Fix bug"),
Task(1, "2026-01-02", "Release"),
Task(2, "2026-01-03", "Refactor"),
Task(1, "2026-01-01", "Deploy"),
]
for t in sorted(tasks):
print(f"P{t.priority} | {t.created_at} | {t.title}")
# P1 | 2026-01-01 | Deploy (priority 1, ранняя дата)
# P1 | 2026-01-02 | Release (priority 1, поздняя дата)
# P2 | 2026-01-01 | Fix bug (priority 2, ранняя дата)
# P2 | 2026-01-03 | Refactor (priority 2, поздняя дата)
Кастомный порядок сортировки
@dataclass(order=True)
class Score:
sort_key: int = field(init=False, repr=False, compare=True)
name: str
value: int
def __post_init__(self):
self.sort_key = -self.value # обратный порядок (высший балл — первым)
scores = [
Score("Bob", 85),
Score("Alice", 92),
Score("Charlie", 78),
]
for s in sorted(scores):
print(f"{s.name}: {s.value}")
# Alice: 92 (высший балл)
# Bob: 85
# Charlie: 78
// Сортировка по нескольким полям:
@dataclass(order=True)
class Employee:
department_sort: int = field(init=False, repr=False, compare=True)
salary_sort: int = field(init=False, repr=False, compare=True)
name: str
department: str
salary: int
def __post_init__(self):
dept_order = {"IT": 0, "HR": 1, "Sales": 2}
self.department_sort = dept_order.get(self.department, 99)
self.salary_sort = -self.salary # высокая зарплата — первая
10. Преобразование в dict / tuple / JSON
asdict — в словарь (рекурсивно)
from dataclasses import dataclass, asdict, astuple
import json
@dataclass
class Address:
city: str
zip_code: str
@dataclass
class Person:
name: str
age: int
address: Address
tags: List[str] = field(default_factory=list)
p = Person("Alice", 30, Address("Москва", "101000"), tags=["admin"])
data = asdict(p)
print(data)
# {'name': 'Alice', 'age': 30,
# 'address': {'city': 'Москва', 'zip_code': '101000'},
# 'tags': ['admin']}
json.dumps(asdict(p), ensure_ascii=False, indent=2)
# Полноценный JSON с русскими буквами
astuple — в кортеж
t = astuple(p)
print(t) # ('Alice', 30, ('Москва', '101000'), ['admin'])
// Полезно для тестов:
assert astuple(p) == ("Alice", 30, ("Москва", "101000"), ["admin"])
// Распаковка как кортеж:
name, age, addr, tags = astuple(p)
Кастомная сериализация в JSON
from datetime import datetime
from uuid import UUID
from decimal import Decimal
class DataclassJSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, UUID):
return str(obj)
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, Decimal):
return float(obj)
if isinstance(obj, set):
return list(obj)
if isinstance(obj, bytes):
return obj.hex()
return super().default(obj)
@dataclass
class Order:
id: UUID
created_at: datetime
total: Decimal
tags: Set[str]
data: bytes
def to_json(obj) -> str:
return json.dumps(asdict(obj), cls=DataclassJSONEncoder, indent=2)
order = Order(
id=UUID("550e8400-e29b-41d4-a716-446655440000"),
created_at=datetime.now(),
total=Decimal("199.99"),
tags={"urgent", "premium"},
data=b"\x00\x01\x02",
)
print(to_json(order))
# {
# "id": "550e8400-e29b-41d4-a716-446655440000",
# "created_at": "2026-06-20T12:34:56.789000",
# "total": 199.99,
# "tags": ["premium", "urgent"],
# "data": "000102"
# }
Десериализация из dict (ручная)
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class Tag:
name: str
color: str = "blue"
@dataclass
class Article:
title: str
tags: List[Tag]
author: Optional[str] = None
# Ручная десериализация с вложенными объектами:
def article_from_dict(data: dict) -> Article:
tags = [Tag(**t) if isinstance(t, dict) else t for t in data["tags"]]
return Article(
title=data["title"],
tags=tags,
author=data.get("author"),
)
raw = {
"title": "My Article",
"tags": [{"name": "python", "color": "blue"}, {"name": "dataclass"}],
}
article = article_from_dict(raw)
print(article)
# Article(title='My Article', tags=[Tag(name='python', color='blue'),
# Tag(name='dataclass', color='blue')], author=None)
11. Сравнение с альтернативами
| Характеристика | dataclass | namedtuple | TypedDict | attrs | pydantic |
|---|---|---|---|---|---|
| Стандартная библиотека | ✅ 3.7+ | ✅ 2.6+ | ✅ 3.8+ | ❌ | ❌ |
| Мутабельность | ✅ / frozen | ❌ | ✅ (нотация) | ✅ | ✅ / frozen |
| Аннотации типов | ✅ | ❌ (строки) | ✅ | ✅ | ✅ строгая |
| Валидация | Ручная post_init | ❌ | ❌ | ✅ @validator | ✅ Field, model_validator |
| Сериализация | asdict, astuple | _asdict | dict() | asdict, serialize | model_dump, model_dump_json |
| Десериализация | Ручная | Ручная | Ручная | @define + converter | model_validate |
| JSON Schema | ❌ | ❌ | ❌ | ❌ (есть add-ons) | ✅ model_json_schema |
| Слоты | ✅ slots=True (3.10+) | ✅ | ❌ | ✅ slots=True | ❌ |
| Generic | ✅ Generic[T] | ❌ | ✅ | ✅ | ✅ BaseModel + Generic |
| Производительность | Высокая | Высокая | Высокая | Средняя | Ниже (валидация) |
| Вес зависимости | 0 KB | 0 KB | 0 KB | ~200 KB | ~2 MB |
Когда что выбрать
- dataclass — 90% случаев: DTO, конфиги, модели без сложной валидации
- namedtuple — лёгкие иммутабельные структуры, совместимость с tuple
- TypedDict — типизация обычных dict'ов (kwargs, конфиги)
- attrs — когда нужно больше, чем dataclass (валидаторы, конвертеры) и нельзя pydantic
- pydantic — строгая валидация, JSON Schema, FastAPI, внешние данные
12. init=False и KW_ONLY
init=False — поле не в init
import hashlib
@dataclass
class User:
username: str
password_hash: str = field(init=False)
def __post_init__(self):
self.password_hash = hashlib.sha256(
self.username.encode()
).hexdigest()
u = User(username="alice")
print(u.password_hash) # вычислен автоматически
KW_ONLY (Python 3.10+)
Маркер KW_ONLY разделяет поля: слева — позиционные, справа — keyword-only.
from dataclasses import dataclass, KW_ONLY
@dataclass
class Product:
name: str # позиционное
price: float # позиционное
_: KW_ONLY
category: str = "general" # keyword-only
discount: float = 0.0 # keyword-only
warehouse: str = "main" # keyword-only
p1 = Product("Laptop", 999.99, category="electronics")
p2 = Product("Mouse", 25.0)
# p3 = Product("Laptop", 999.99, "electronics") # TypeError!
// Позиционные — обязательные, keyword-only — опциональные с умолчаниями
kw_only на уровне класса vs KW_ONLY
// kw_only=True — все поля keyword-only
@dataclass(kw_only=True)
class Person:
name: str
age: int
Person(name="Alice", age=30) # OK
# Person("Alice", 30) # TypeError
// KW_ONLY — только часть полей keyword-only
@dataclass
class Order:
id: int
items: List[str]
_: KW_ONLY
coupon: str = ""
priority: str = "normal"
// Комбинированный пример:
@dataclass
class APIRequest:
method: str # позиционное
path: str # позиционное
_: KW_ONLY
headers: Dict[str, str] = field(default_factory=dict) # keyword-only
body: Any = None # keyword-only
timeout: int = 30 # keyword-only
req = APIRequest("GET", "/users", headers={"Auth": "Bearer x"}, timeout=10)
13. Слоты (slots)
slots=True (3.10+) генерирует класс с __slots__ — меньше памяти, быстрее доступ к полям.
Сравнение памяти
import sys
@dataclass
class Regular:
x: int
y: int
z: int
@dataclass(slots=True)
class Slotted:
x: int
y: int
z: int
r = Regular(1, 2, 3)
s = Slotted(1, 2, 3)
print(r.__dict__) # {'x': 1, 'y': 2, 'z': 3}
# print(s.__dict__) # AttributeError: 'Slotted' object has no attribute '__dict__'
print(sys.getsizeof(r)) # 48 bytes + __dict__ (≈ 200+ bytes всего)
print(sys.getsizeof(s)) # 32 bytes (только значения в слотах)
// Массовое создание (100000 экземпляров):
# Regular: ~ 250 MB
# Slotted: ~ 56 MB
Ограничения slots
# 1. Нельзя добавить новое поле динамически
s = Slotted(1, 2, 3)
# s.new_attr = 5 # AttributeError
# 2. Наследование: __dict__ появляется, если родитель без slots
@dataclass
class Base:
x: int
@dataclass(slots=True)
class Child(Base):
y: int
c = Child(1, 2)
print(hasattr(c, '__dict__')) # True! (из-за Base)
# 3. weakref — нужно добавлять явно
@dataclass(slots=True, weakref_slot=True) # 3.11+
class Weakable:
x: int
# 4. Нет множественного наследования со slots
@dataclass(slots=True)
class A: x: int
@dataclass(slots=True)
class B: y: int
# class C(A, B): pass # TypeError: multiple bases have instance lay-out conflict
14. Дженерики (Generic dataclass)
Generic dataclass
from typing import Generic, TypeVar, List, Dict, Optional
T = TypeVar("T")
V = TypeVar("V")
K = TypeVar("K")
@dataclass
class Pair(Generic[T, V]):
first: T
second: V
@dataclass
class Response(Generic[T]):
data: T
status: int
message: str = ""
@dataclass
class Paginated(Generic[T]):
items: List[T]
total: int
page: int
page_size: int
# Использование:
pair = Pair[int, str](1, "hello")
resp = Response[List[int]]([1, 2, 3], 200)
page = Paginated[str](["a", "b"], total=2, page=1, page_size=10)
reveal_type(pair.first) # int
reveal_type(resp.data[0]) # int
reveal_type(page.items) # List[str]
Generic с ограничениями
# TypeVar с bound
N = TypeVar("N", int, float)
Comparable = TypeVar("Comparable", bound="ComparableProtocol")
@dataclass
class Range(Generic[N]):
min: N
max: N
def contains(self, value: N) -> bool:
return self.min <= value <= self.max
r = Range[int](1, 10)
print(r.contains(5)) # True
# Range[str]("a", "z") # mypy ошибка (str не int|float)
// Generic + default_factory
@dataclass
class Stack(Generic[T]):
items: List[T] = field(default_factory=list)
def push(self, item: T) -> None:
self.items.append(item)
def pop(self) -> Optional[T]:
return self.items.pop() if self.items else None
s = Stack[int]()
s.push(1)
s.push(2)
print(s.pop()) # 2
Generic + наследование
@dataclass
class IntPair(Pair[int, int]):
"""Pair[int, int] с методом sum"""
def sum(self) -> int:
return self.first + self.second
def product(self) -> int:
return self.first * self.second
ip = IntPair(3, 4)
print(ip.sum()) # 7
print(ip.product()) # 12
// Generic с несколькими TypeVar:
S = TypeVar("S")
T = TypeVar("T")
@dataclass
class BiMap(Generic[S, T]):
forward: Dict[S, T] = field(default_factory=dict)
backward: Dict[T, S] = field(default_factory=dict)
def add(self, key: S, value: T) -> None:
self.forward[key] = value
self.backward[value] = key
def get(self, key: S) -> Optional[T]:
return self.forward.get(key)
def inverse(self, value: T) -> Optional[S]:
return self.backward.get(value)
15. InitVar
InitVar — переменная, передаваемая в __init__, но не становящаяся полем экземпляра. Доступна только в __post_init__. Идеально для «временных» параметров конструктора.
Базовый пример
from dataclasses import dataclass, InitVar, field
from typing import Optional
@dataclass
class DatabaseConnection:
host: str
port: int
db_url: str = field(init=False)
# InitVar — передаётся в init, но не сохраняется
password: InitVar[Optional[str]] = None
def __post_init__(self, password: Optional[str]):
# password доступен ТОЛЬКО здесь
if password:
self.db_url = f"postgresql://user:{password}@{self.host}:{self.port}/db"
else:
self.db_url = f"postgresql://@{self.host}:{self.port}/db"
conn = DatabaseConnection("localhost", 5432, password="secret")
print(conn.db_url) # postgresql://user:secret@localhost:5432/db
# print(conn.password) # AttributeError: 'DatabaseConnection' object has no attribute 'password'
Пример: dependency injection через InitVar
@dataclass
class Service:
repo: "Repository" = field(init=False)
logger: "Logger" = field(init=False)
# Внешние зависимости — InitVar
db_session: InitVar["Session"]
log_level: InitVar[str] = "INFO"
def __post_init__(self, db_session, log_level):
self.repo = Repository(db_session)
self.logger = Logger(level=log_level)
# Использование:
session = create_session()
service = Service(db_session=session, log_level="DEBUG")
# service.db_session — недоступен, session живёт только в post_init
InitVar с наследованием
@dataclass
class Base:
name: str
config: InitVar[dict] = field(default_factory=dict)
def __post_init__(self, config: dict):
print(f"Base: config keys = {list(config.keys())}")
@dataclass
class Child(Base):
extra: str = ""
# При наследовании InitVar ДОЛЖЕН быть передан
# и проброшен в super().__post_init__
def __post_init__(self, config: dict):
super().__post_init__(config) # проброс обязателен!
print(f"Child: extra = {self.extra}")
c = Child(name="test", config={"key": "val"}, extra="data")
# Base: config keys = ['key']
# Child: extra = data
InitVar для тестов
@dataclass
class PaymentProcessor:
gateway: str = "stripe"
api_key: InitVar[str] = ""
_client: Any = field(init=False)
def __post_init__(self, api_key: str):
if not api_key:
api_key = os.environ.get("PAYMENT_API_KEY", "")
self._client = self._create_client(api_key)
def _create_client(self, api_key: str):
# В тестах — заглушка
return {"api_key": api_key, "gateway": self.gateway}
// В тестах:
@pytest.fixture
def processor():
return PaymentProcessor(api_key="test_key_123")
// В проде:
# processor = PaymentProcessor() # api_key из переменной окружения
16. ClassVar
ClassVar[T] — аннотация, помечающая поле как классовое (статическое), а не поле экземпляра. Dataclass полностью игнорирует такие поля — они не попадают в __init__, __repr__, __eq__.
Базовый пример
from dataclasses import dataclass, field
from typing import ClassVar, List
@dataclass
class Employee:
name: str
salary: float
# ClassVar — не поле, не в init/repr/eq
company: ClassVar[str] = "ACME Inc."
employee_count: ClassVar[int] = 0
all_employees: ClassVar[List["Employee"]] = []
def __post_init__(self):
Employee.employee_count += 1
Employee.all_employees.append(self)
e1 = Employee("Alice", 100_000)
e2 = Employee("Bob", 80_000)
print(e1) # Employee(name='Alice', salary=100000.0) — нет company!
print(Employee.company) # "ACME Inc." — через класс
print(Employee.employee_count) # 2
print(Employee.all_employees) # [e1, e2]
# Изменение ClassVar влияет на ВСЕ экземпляры:
Employee.company = "New Corp"
print(e2.company) # "New Corp"
ClassVar для конфигурации
@dataclass
class APIClient:
base_url: str
timeout: int = 30
# Глобальные настройки для всех экземпляров
default_headers: ClassVar[Dict[str, str]] = {
"User-Agent": "MyApp/1.0",
"Accept": "application/json",
}
retry_count: ClassVar[int] = 3
_instance_count: ClassVar[int] = 0
def __post_init__(self):
APIClient._instance_count += 1
@classmethod
def set_global_timeout(cls, timeout: int) -> None:
cls.default_timeout = timeout
client1 = APIClient("https://api.example.com")
client2 = APIClient("https://api2.example.com", timeout=10)
print(APIClient.default_headers) # общие заголовки
print(APIClient._instance_count) # 2
ClassVar vs field(init=False)
@dataclass
class Example:
# ClassVar — не поле dataclass, чисто классовое
class_var: ClassVar[int] = 42
# init=False — поле экземпляра, но не в init
instance_field: int = field(init=False, default=0)
e = Example()
print(e.class_var) # 42 — доступ через экземпляр (унаследовано)
print(Example.class_var) # 42 — доступ через класс
print(e.instance_field) # 0 — есть у экземпляра
# ClassVar можно менять только через класс:
Example.class_var = 100
print(e.class_var) # 100
# instance_field уникален для каждого экземпляра:
e2 = Example()
e2.instance_field = 5
print(e.instance_field) # 0
print(e2.instance_field) # 5
17. __dataclass_fields__ — интроспекция
__dataclass_fields__ — словарь всех полей dataclass. Ключ — имя поля, значение — объект Field с метаданными.
Структура Field
@dataclass
class Config:
host: str = field(metadata={"env": "HOST"})
port: int = field(default=8080, metadata={"env": "PORT", "min": 1})
debug: bool = field(default=False, repr=False)
field_map = Config.__dataclass_fields__
for name, f in field_map.items():
print(f"{name}:")
print(f" type={f.type}") # / /
print(f" default={f.default}") # MISSING / 8080 / False
print(f" init={f.init}") # True / True / True
print(f" repr={f.repr}") # True / True / False
print(f" compare={f.compare}") # True / True / True
print(f" hash={f.hash}") # None / None / None
print(f" metadata={f.metadata}") # {'env': 'HOST'} / ...
# Output:
# host: type=, metadata={'env': 'HOST'}
# port: type=, default=8080, metadata={'env': 'PORT', 'min': 1}
# debug: type=, default=False, repr=False
Практические применения
// 1. Генерация JSON Schema из метаданных
def to_json_schema(cls) -> dict:
schema = {"type": "object", "properties": {}}
for name, f in cls.__dataclass_fields__.items():
if f.init: # только init-поля
prop = {}
if f.metadata:
prop.update(f.metadata)
if f.default is not dataclasses.MISSING:
prop["default"] = f.default
schema["properties"][name] = prop
return schema
print(to_json_schema(Config))
# {'type': 'object', 'properties': {
# 'host': {'env': 'HOST'},
# 'port': {'default': 8080, 'env': 'PORT', 'min': 1},
# 'debug': {'default': False}
# }}
// 2. Загрузка из env / dict с авто-маппингом
def from_env(cls) -> object:
import os
kwargs = {}
for name, f in cls.__dataclass_fields__.items():
env_name = f.metadata.get("env", name.upper())
if env_name in os.environ:
val = os.environ[env_name]
# Простейшее приведение типов
if f.type is int:
val = int(val)
elif f.type is bool:
val = val.lower() in ("true", "1", "yes")
kwargs[name] = val
return cls(**kwargs)
config = from_env(Config) # читает HOST, PORT, DEBUG из os.environ
// 3. Создание dataclass из dict с игнорированием лишних ключей
def from_dict(cls, data: dict):
field_names = set(cls.__dataclass_fields__.keys())
filtered = {k: v for k, v in data.items() if k in field_names}
return cls(**filtered)
raw = {"host": "localhost", "port": 8080, "unknown": "ignored"}
config = from_dict(Config, raw)
18. Сериализация: cattrs / dataclasses-json / mashumaro
Для автоматической десериализации (dict → dataclass, JSON → dataclass) используйте специализированные библиотеки.
cattrs
Лёгкая, быстрая, кастомизируемая:
# pip install cattrs
from cattrs import structure, unstructure
from dataclasses import dataclass
from typing import List, Optional
from datetime import datetime
@dataclass
class Tag:
name: str
color: str = "blue"
@dataclass
class Post:
id: int
title: str
tags: List[Tag]
created_at: datetime
author: Optional[str] = None
# Сериализация (dataclass → dict):
data = unstructure(Post(
id=1,
title="Hello",
tags=[Tag("python"), Tag("web", "green")],
created_at=datetime(2026, 6, 20, 12, 0),
))
# {'id': 1, 'title': 'Hello', 'tags': [{'name': 'python', 'color': 'blue'},
# {'name': 'web', 'color': 'green'}], 'created_at': datetime(2026, 6, 20, 12, 0)}
# Десериализация (dict → dataclass):
raw = {
"id": 1,
"title": "Hello",
"tags": [{"name": "python"}, {"name": "web", "color": "green"}],
"created_at": "2026-06-20T12:00:00",
"author": "Alice",
}
post = structure(raw, Post)
# Post(id=1, title='Hello', tags=[Tag(name='python', color='blue'),
# Tag(name='web', color='green')], created_at=datetime(2026, 6, 20, 12, 0),
# author='Alice')
// Кастомные преобразователи:
from cattrs import Converter
from uuid import UUID
conv = Converter()
conv.register_unstructure_hook(UUID, str)
conv.register_structure_hook(UUID, lambda v, _: UUID(v))
@dataclass
class Event:
id: UUID
name: str
event = structure({"id": "550e8400-e29b-41d4-a716-446655440000", "name": "test"}, Event)
print(event) # Event(id=UUID(...), name='test')
dataclasses-json
Декоратор, добавляющий from_json / to_json прямо на класс:
# pip install dataclasses-json
from dataclasses_json import dataclass_json
from dataclasses import dataclass
from typing import List, Optional
@dataclass_json
@dataclass
class User:
id: int
name: str
email: str
roles: List[str] = field(default_factory=list)
# Сериализация:
user = User(id=1, name="Alice", email="a@b.com", roles=["admin"])
json_str = user.to_json() # {"id": 1, "name": "Alice", ...}
data = user.to_dict() # {"id": 1, "name": "Alice", ...}
# Десериализация:
user2 = User.from_json('{"id": 2, "name": "Bob", "email": "b@c.com"}')
user3 = User.from_dict({"id": 3, "name": "Charlie", "email": "c@d.com"})
// Настройка имён полей (camelCase → snake_case):
from dataclasses_json import config
@dataclass_json
@dataclass
class ApiResponse:
created_at: str = field(metadata=config(field_name="createdAt"))
updated_at: str = field(metadata=config(field_name="updatedAt"))
resp = ApiResponse.from_json('{"createdAt": "2026-01-01", "updatedAt": "2026-06-20"}')
print(resp) # ApiResponse(created_at='2026-01-01', updated_at='2026-06-20')
mashumaro
Самая быстрая, строгая типизация, поддержка Union, Optional, Generic:
# pip install mashumaro
from mashumaro import DataClassDictMixin
from dataclasses import dataclass
from typing import Optional, List, Union
@dataclass
class Config(DataClassDictMixin):
host: str
port: int
debug: bool = False
tags: Optional[List[str]] = None
metadata: Union[str, dict, None] = None
config = Config.from_dict({
"host": "localhost",
"port": 8080,
"tags": ["dev", "test"],
})
print(config.to_json()) # {"host": "localhost", "port": 8080, ...}
// Валидация Union типов:
@dataclass
class Result(DataClassDictMixin):
status: str
data: Union[List[int], dict, None] = None
r1 = Result.from_dict({"status": "ok", "data": [1, 2, 3]})
r2 = Result.from_dict({"status": "ok", "data": {"key": "val"}})
r3 = Result.from_dict({"status": "error"})
Сравнение библиотек
| Библиотека | Скорость | Вес | Generic | Union | Кастомизация | JSON Schema |
|---|---|---|---|---|---|---|
| cattrs | Высокая | ~200 KB | ✅ | ✅ | Высокая (Converter) | ❌ |
| dataclasses-json | Средняя | ~100 KB | ✅ | ❌ (ограничено) | Средняя (metadata) | ❌ |
| mashumaro | Очень высокая | ~150 KB | ✅ | ✅ | Высокая (builder) | ❌ |
| pydantic | Ниже | ~2 MB | ✅ | ✅ | Высокая (Field, validator) | ✅ |
19. Dataclass + FastAPI
FastAPI поддерживает dataclass как request/response модели (начиная с версии 0.95). Хотя pydantic — основной инструмент, dataclass можно использовать через Body(), Query() и другие зависимоcти.
Request body (POST/PUT)
from fastapi import FastAPI, Body, Query, Path, Depends
from dataclasses import dataclass, field
from typing import Optional, List
from datetime import datetime
from uuid import UUID, uuid4
app = FastAPI()
@dataclass
class CreateUserRequest:
name: str
email: str
age: Optional[int] = None
tags: List[str] = field(default_factory=list)
@dataclass
class UserResponse:
id: UUID
name: str
email: str
age: Optional[int] = None
created_at: datetime
tags: List[str] = field(default_factory=list)
users_db: dict = {}
@app.post("/users", response_model=UserResponse)
def create_user(data: CreateUserRequest):
user = UserResponse(
id=uuid4(),
name=data.name,
email=data.email,
age=data.age,
created_at=datetime.now(),
tags=data.tags,
)
users_db[user.id] = user
return user
@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: UUID):
if user_id not in users_db:
from fastapi import HTTPException
raise HTTPException(404, "User not found")
return users_db[user_id]
Query и Path параметры
@dataclass
class Pagination:
page: int = Query(1, ge=1)
page_size: int = Query(10, ge=1, le=100)
sort_by: str = Query("created_at")
order: str = Query("desc", pattern="^(asc|desc)$")
@dataclass
class DateRange:
from_date: Optional[str] = Query(None, alias="from")
to_date: Optional[str] = Query(None, alias="to")
@app.get("/users")
def list_users(
pagination: Pagination = Depends(),
dates: DateRange = Depends(),
):
return {
"page": pagination.page,
"page_size": pagination.page_size,
"from": dates.from_date,
"to": dates.to_date,
}
// Запрос: GET /users?page=2&page_size=20&from=2026-01-01
// Ответ: {"page": 2, "page_size": 20, "from": "2026-01-01", "to": null}
Response с фильтрацией полей
@dataclass
class UserPublic:
id: UUID
name: str
@dataclass
class UserAdmin(UserPublic):
email: str
role: str
is_active: bool
last_login: Optional[datetime] = None
@app.get("/users/{user_id}/public", response_model=UserPublic)
def get_public_user(user_id: UUID):
user = users_db[user_id]
return UserPublic(id=user.id, name=user.name)
@app.get("/users/{user_id}/admin", response_model=UserAdmin)
def get_admin_user(user_id: UUID):
user = users_db[user_id]
return UserAdmin(
id=user.id, name=user.name, email=user.email,
role="admin", is_active=True, last_login=datetime.now(),
)
20. Dataclass + SQLAlchemy
SQLAlchemy поддерживает dataclass как модели через аннотированный декларативный стиль (2.0+) и MappedAsDataclass.
MappedAsDataclass (SQLAlchemy 2.0+)
# pip install sqlalchemy
from sqlalchemy import create_engine, String, Integer, Float, DateTime, ForeignKey, select
from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass, Mapped, mapped_column
from sqlalchemy.orm import relationship
from datetime import datetime
from typing import List, Optional
from decimal import Decimal
class Base(DeclarativeBase, MappedAsDataclass):
"""Базовый класс: SQLAlchemy + dataclass"""
pass
@dataclass
class Product(Base):
__tablename__ = "products"
id: Mapped[int] = mapped_column(primary_key=True, init=False)
name: Mapped[str] = mapped_column(String(255))
price: Mapped[Decimal] = mapped_column(Float) # Decimal → Float в БД
created_at: Mapped[datetime] = mapped_column(
DateTime, default_factory=datetime.now, init=False
)
category_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("categories.id"), default=None
)
# relationship — не поле dataclass
category: Mapped[Optional["Category"]] = relationship(default=None)
@dataclass
class Category(Base):
__tablename__ = "categories"
id: Mapped[int] = mapped_column(primary_key=True, init=False)
name: Mapped[str] = mapped_column(String(100))
products: Mapped[List[Product]] = relationship(default_factory=list)
# Использование:
engine = create_engine("sqlite:///:memory:", echo=True)
Base.metadata.create_all(engine)
from sqlalchemy.orm import Session
with Session(engine) as session:
cat = Category(name="Electronics")
session.add(cat)
session.flush()
product = Product(name="Laptop", price=999.99, category_id=cat.id)
session.add(product)
session.commit()
# Чтение:
products = session.execute(select(Product)).scalars().all()
for p in products:
print(f"{p.name}: ${p.price}")
Альтернатива: @dataclass + обычная SQLAlchemy модель
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
@dataclass
class UserDTO:
"""DTO для передачи данных между слоями"""
id: Optional[int] = None
name: str = ""
email: str = ""
created_at: Optional[datetime] = None
# SQLAlchemy модель (отдельно):
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.ext.declarative import declarative_base
SQLBase = declarative_base()
class UserModel(SQLBase):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String(255))
email = Column(String(255))
created_at = Column(DateTime, default=datetime.now)
def to_dto(self) -> UserDTO:
return UserDTO(
id=self.id,
name=self.name,
email=self.email,
created_at=self.created_at,
)
@classmethod
def from_dto(cls, dto: UserDTO) -> "UserModel":
return cls(
name=dto.name,
email=dto.email,
)
# Использование:
user_dto = UserDTO(name="Alice", email="a@b.com")
user_model = UserModel.from_dto(user_dto)
session.add(user_model)
session.commit()
# При чтении:
db_model = session.get(UserModel, 1)
dto = db_model.to_dto()
21. PEP 681: dataclass_transform
PEP 681 (Python 3.11+) — декоратор typing.dataclass_transform, который позволяет библиотекам помечать свои декораторы как «dataclass-подобные». mypy и pyright понимают, что декорированный класс — dataclass, и проверяют типы полей соответственно.
Проблема
# Библиотека определяет свой декоратор:
from my_lib import model
@model
class User:
name: str = ""
age: int = 0
user = User(name="Alice")
print(user.name) # mypy не знает, что name — поле!
# mypy: "User" has no attribute "name"
Решение
from typing import dataclass_transform, TypeVar, Callable, Any
T = TypeVar("T")
@dataclass_transform(
eq_default=True, # по умолч. eq=True
order_default=False, # по умолч. order=False
kw_only_default=False, # по умолч. kw_only=False
frozen_default=False, # по умолч. frozen=False
field_specifiers=(),
)
def model(cls: type[T]) -> type[T]:
"""Кастомный декоратор, работающий как dataclass"""
# Внутри использует стандартный @dataclass
from dataclasses import dataclass
return dataclass(cls)
# Теперь mypy понимает:
@model
class User:
name: str = ""
age: int = 0
user = User(name="Alice")
print(user.name) # mypy OK
// С поддержкой field_specifiers:
from typing import dataclass_transform
def my_field(*, default: Any = dataclasses.MISSING, **kwargs):
return dataclasses.field(default=default, **kwargs)
@dataclass_transform(field_specifiers=(my_field,))
def entity(cls):
return dataclass(cls)
@entity
class Product:
name: str
price: float = my_field(default=0.0, metadata={"currency": "USD"})
# mypy понимает и my_field как field
Полный пример библиотеки
from typing import dataclass_transform, TypeVar
from dataclasses import dataclass, field
C = TypeVar("C")
@dataclass_transform(
eq_default=True,
order_default=False,
frozen_default=False,
kw_only_default=False,
field_specifiers=(field,),
)
def record(cls: type[C]) -> type[C]:
"""Декоратор 'record' — аналог dataclass"""
return dataclass(cls)
@record
class Point:
x: int
y: int
# mypy видит Point как dataclass с полями x, y
p = Point(1, 2)
p.x = 5 # OK
@record
@dataclass(frozen=True) # можно комбинировать
class FrozenPoint:
x: int
y: int
fp = FrozenPoint(1, 2)
# fp.x = 5 # mypy: FrozenInstanceError!
22. Frozen + mutable вложенные объекты
frozen=True защищает только верхний уровень. Вложенные list, dict, dataclass остаются мутабельными. Это называется shallow freeze.
Shallow freeze — проблема
@dataclass(frozen=True)
class ShoppingCart:
items: List[str] = field(default_factory=list)
metadata: Dict[str, str] = field(default_factory=dict)
cart = ShoppingCart()
# cart.items = ["apple"] # FrozenInstanceError — OK
cart.items.append("apple") # ✅ работает! список мутабелен
cart.metadata["coupon"] = "SAVE10" # ✅ dict тоже мутабелен
print(cart) # ShoppingCart(items=['apple'], metadata={'coupon': 'SAVE10'})
# Frozen не защитил содержимое list и dict!
Глубокое замораживание (deep freeze)
from typing import List, Dict, Tuple, Any
from copy import deepcopy
def deep_freeze(obj):
"""Рекурсивно замораживает вложенные структуры"""
if isinstance(obj, dict):
return Tuple(sorted((k, deep_freeze(v)) for k, v in obj.items()))
if isinstance(obj, (list, tuple)):
return Tuple(deep_freeze(item) for item in obj)
if isinstance(obj, set):
return Tuple(sorted(deep_freeze(item) for item in obj))
return obj
@dataclass(frozen=True)
class ImmutableCart:
items: Tuple[str, ...] = field(default_factory=tuple)
metadata: Tuple[Tuple[str, str], ...] = field(default_factory=tuple)
tags: Tuple[str, ...] = field(default_factory=tuple)
@classmethod
def from_mutable(cls, items=None, metadata=None, tags=None):
return cls(
items=tuple(items) if items else (),
metadata=deep_freeze(metadata or {}),
tags=tuple(tags or []),
)
cart = ImmutableCart.from_mutable(
items=["apple", "banana"],
metadata={"coupon": "SAVE10"},
tags=["fresh"],
)
# cart.items[0] = "orange" # TypeError: 'tuple' object does not support item assignment
// Решение через property + private field:
@dataclass(frozen=True)
class SafeCart:
_items: Tuple[str, ...] = field(default_factory=tuple, repr=False)
_metadata: Tuple[Tuple[str, str], ...] = field(default_factory=tuple, repr=False)
@property
def items(self) -> Tuple[str, ...]:
return self._items
@property
def metadata(self) -> dict:
return dict(self._metadata)
cart = SafeCart(
_items=("apple",),
_metadata=(("key", "val"),),
)
print(cart.items) # ('apple',) — кортеж, не list
Кастомный декоратор deep_frozen
import functools
def deep_frozen(cls):
"""Декоратор, делающий dataclass глубоко неизменяемым"""
original_post_init = getattr(cls, "__post_init__", None)
def new_post_init(self, *args, **kwargs):
if original_post_init:
original_post_init(self, *args, **kwargs)
# Заменить все list → tuple, dict → frozenset of tuples
for name, f in cls.__dataclass_fields__.items():
val = getattr(self, name)
if isinstance(val, list):
object.__setattr__(self, name, tuple(val))
elif isinstance(val, dict):
object.__setattr__(self, name, tuple(sorted(val.items())))
elif isinstance(val, set):
object.__setattr__(self, name, tuple(sorted(val)))
cls.__post_init__ = new_post_init
return cls
@deep_frozen
@dataclass(frozen=True)
class ImmutableConfig:
name: str
tags: List[str] = field(default_factory=list)
options: Dict[str, str] = field(default_factory=dict)
cfg = ImmutableConfig(
name="app",
tags=["dev", "staging"],
options={"timeout": "30"},
)
print(cfg.tags) # ('dev', 'staging') — tuple, не list
print(type(cfg.options)) # — (('timeout', '30'),)
23. Производительность
Скорость создания экземпляров
# pip install simple_benchmark
from simple_benchmark import benchmark
from dataclasses import dataclass, field
from typing import List
from collections import namedtuple
# ---- Test classes ----
class PlainClass:
def __init__(self, x: int, y: int, z: int):
self.x = x
self.y = y
self.z = z
@dataclass
class Dataclass:
x: int
y: int
z: int
@dataclass(slots=True)
class DataclassSlots:
x: int
y: int
z: int
NT = namedtuple("NT", ["x", "y", "z"])
# ---- Benchmarks ----
# Создание 100000 экземпляров:
# PlainClass: 0.12s (baseline)
# Dataclass: 0.14s (+17%) — практически не отличается
# DataclassSlots: 0.13s (+8%) — чуть быстрее за счёт slots
# namedtuple: 0.09s (-25%) — быстрее всех (C-уровень)
# Доступ к полям (1000000 операций):
# PlainClass: 0.02s
# Dataclass: 0.02s
# DataclassSlots: 0.015s (-25%) — slots ускоряет доступ
# namedtuple: 0.02s
# Потребление памяти (100000 экземпляров):
# PlainClass: 45 MB (с __dict__)
# Dataclass: 45 MB (с __dict__)
# DataclassSlots: 12 MB (-73%) — без __dict__
# namedtuple: 12 MB (-73%) — тоже без __dict__
Детальные замеры
| Операция | plain class | dataclass | dataclass+slots | namedtuple | attrs | pydantic |
|---|---|---|---|---|---|---|
| Создание (100k) | 0.12s | 0.14s | 0.13s | 0.09s | 0.25s | 1.2s |
| Доступ к полю (1M) | 0.020s | 0.020s | 0.015s | 0.020s | 0.022s | 0.025s |
| Сериализация (100k) | 0.05s | 0.05s | 0.05s | 0.04s | 0.06s | 0.20s |
| Память (100k) | 45 MB | 45 MB | 12 MB | 12 MB | 45 MB | 55 MB |
| Размер __dict__ | ~400 bytes | ~400 bytes | — | — | ~400 bytes | ~500 bytes |
dataclass практически не уступает plain class по скорости. 2) slots=True даёт -73% памяти и +25% скорости доступа. 3) pydantic в 8-10x медленнее на создании (из-за валидации). 4) namedtuple быстрее всех, но менее гибкий.Рекомендации по производительности
- Для hot-path (тысячи созданий в секунду) —
dataclass(slots=True)или namedtuple - Для DTO в веб-приложениях разница незаметна — используйте обычный dataclass
- Если нужна валидация на hot-path — предварительно валидируйте данные, потом создавайте dataclass
- Избегайте сложной логики в
__post_init__для часто создаваемых объектов
24. Dataclass + ABC / Protocol
Dataclass можно комбинировать с абстрактными классами (ABC) и протоколами (Protocol) для дизайна по контракту.
Dataclass + ABC
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List
@dataclass
class BaseModel(ABC):
id: int
name: str
@abstractmethod
def validate(self) -> bool:
"""Валидация модели — обязателен к реализации"""
...
@abstractmethod
def to_dict(self) -> dict:
...
@dataclass
class UserModel(BaseModel):
email: str
roles: List[str]
def validate(self) -> bool:
return bool(self.name and "@" in self.email)
def to_dict(self) -> dict:
return {"id": self.id, "name": self.name, "email": self.email}
@dataclass
class ProductModel(BaseModel):
price: float
stock: int
def validate(self) -> bool:
return self.price > 0 and self.stock >= 0
def to_dict(self) -> dict:
return {"id": self.id, "name": self.name, "price": self.price}
# Полиморфизм:
models: List[BaseModel] = [
UserModel(1, "Alice", "a@b.com", ["admin"]),
ProductModel(2, "Laptop", 999.99, 10),
]
for m in models:
print(f"{m.name}: valid={m.validate()}")
Dataclass + Protocol (структурная типизация)
from typing import Protocol, runtime_checkable
from dataclasses import dataclass
@runtime_checkable
class Serializable(Protocol):
"""Протокол: что-то, что можно сериализовать в dict"""
def to_dict(self) -> dict: ...
@runtime_checkable
class Named(Protocol):
"""Протокол: что-то с именем"""
name: str
@dataclass
class Person:
name: str
age: int
def to_dict(self) -> dict:
return {"name": self.name, "age": self.age}
@dataclass
class Company:
name: str
employees: list
def to_dict(self) -> dict:
return {"name": self.name, "count": len(self.employees)}
def save(obj: Serializable) -> None:
data = obj.to_dict()
print(f"Saving: {data}")
def greet(obj: Named) -> str:
return f"Hello, {obj.name}!"
person = Person("Alice", 30)
company = Company("ACME", ["Bob", "Charlie"])
save(person) # Saving: {'name': 'Alice', 'age': 30}
save(company) # Saving: {'name': 'ACME', 'count': 2}
print(greet(person)) # Hello, Alice!
print(greet(company)) # Hello, ACME!
# runtime_checkable позволяет использовать isinstance:
print(isinstance(person, Serializable)) # True
print(isinstance(person, Named)) # True
Factory pattern с dataclass
from abc import ABC, abstractmethod
from typing import Dict, Type
@dataclass
class Config(ABC):
@abstractmethod
def get_connection_string(self) -> str: ...
@dataclass
class PostgresConfig(Config):
host: str = "localhost"
port: int = 5432
db: str = "app"
user: str = "admin"
password: str = ""
def get_connection_string(self) -> str:
return f"postgresql://{self.user}:****@{self.host}:{self.port}/{self.db}"
@dataclass
class RedisConfig(Config):
host: str = "localhost"
port: int = 6379
db: int = 0
def get_connection_string(self) -> str:
return f"redis://{self.host}:{self.port}/{self.db}"
class ConfigFactory:
_registry: Dict[str, Type[Config]] = {
"postgres": PostgresConfig,
"redis": RedisConfig,
}
@classmethod
def create(cls, db_type: str, **kwargs) -> Config:
config_cls = cls._registry.get(db_type)
if not config_cls:
raise ValueError(f"Unknown db type: {db_type}")
return config_cls(**kwargs)
# Использование:
pg = ConfigFactory.create("postgres", host="prod-db.example.com")
redis = ConfigFactory.create("redis", port=6380)
print(pg.get_connection_string()) # postgresql://admin:****@prod-db.example.com:5432/app
print(redis.get_connection_string()) # redis://localhost:6380/0
25. Продвинутые техники
Дескрипторы (property-like поля с валидацией)
class ValidatedField:
"""Дескриптор с валидацией для dataclass"""
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if value is None:
raise ValueError(f"{self.name} cannot be None")
obj.__dict__[self.name] = value
@dataclass
class User:
name: str
email: str
age: int = 0
# Дескриптор для поля — валидация при присвоении
_role = ValidatedField()
@property
def role(self):
return self._role
@role.setter
def role(self, value):
allowed = {"admin", "user", "moderator"}
if value not in allowed:
raise ValueError(f"Role must be one of {allowed}")
self._role = value
u = User("Alice", "a@b.com")
u.role = "admin"
print(u.role) # "admin"
# u.role = "superadmin" # ValueError
Dataclass как декоратор
from functools import wraps
from typing import Callable, Any
def dataclass_cache(maxsize: int = 128):
"""Декоратор, кэширующий результаты функции как dataclass"""
def decorator(func: Callable) -> Callable:
@dataclass(frozen=True)
class CacheEntry:
args: tuple
kwargs: frozenset
result: Any
cache: dict = {}
from functools import lru_cache
@wraps(func)
def wrapper(*args, **kwargs):
key = (args, frozenset(kwargs.items()))
if key not in cache:
result = func(*args, **kwargs)
cache[key] = CacheEntry(args, frozenset(kwargs.items()), result)
if len(cache) > maxsize:
cache.pop(next(iter(cache)))
return cache[key].result
return wrapper
return decorator
@dataclass_cache(maxsize=10)
def expensive_calc(x: int, y: int) -> int:
print(f"Calculating {x} + {y}...")
return x + y
print(expensive_calc(1, 2)) # Calculating... → 3
print(expensive_calc(1, 2)) # из кэша → 3
Dataclass с кастомным конструктором (classmethod)
import re
from typing import Optional
@dataclass
class Email:
local: str
domain: str
full: str = field(init=False, repr=False)
def __post_init__(self):
self.full = f"{self.local}@{self.domain}"
@classmethod
def parse(cls, email_str: str) -> Optional["Email"]:
"""Парсинг email из строки"""
match = re.match(r"^(.+)@(.+)$", email_str.strip())
if match:
return cls(local=match.group(1), domain=match.group(2))
return None
@classmethod
def from_parts(cls, local: str, domain: str) -> "Email":
return cls(local=local, domain=domain)
def __repr__(self):
return f"Email({self.full})"
# Разные способы создания:
e1 = Email.parse("alice@example.com")
e2 = Email.from_parts("bob", "test.org")
e3 = Email(local="charlie", domain="company.com")
print(e1) # Email(alice@example.com)
print(e2) # Email(bob@test.org)
State machine через dataclass
from enum import Enum, auto
from typing import Dict, Callable
class OrderState(Enum):
PENDING = auto()
CONFIRMED = auto()
SHIPPED = auto()
DELIVERED = auto()
CANCELLED = auto()
@dataclass
class Order:
id: str
state: OrderState = OrderState.PENDING
history: List[str] = field(default_factory=list)
# Машина состояний
_transitions: Dict[OrderState, Dict[str, OrderState]] = field(
default_factory=lambda: {
OrderState.PENDING: {
"confirm": OrderState.CONFIRMED,
"cancel": OrderState.CANCELLED,
},
OrderState.CONFIRMED: {
"ship": OrderState.SHIPPED,
"cancel": OrderState.CANCELLED,
},
OrderState.SHIPPED: {
"deliver": OrderState.DELIVERED,
},
},
compare=False, hash=False, repr=False,
)
def transition(self, action: str) -> "Order":
current = self._transitions.get(self.state, {})
next_state = current.get(action)
if next_state is None:
raise ValueError(
f"Cannot {action} from state {self.state.name}"
)
self.state = next_state
self.history.append(
f"{action} → {next_state.name}"
)
return self
order = Order(id="ORD-001")
order.transition("confirm") # PENDING → CONFIRMED
order.transition("ship") # CONFIRMED → SHIPPED
# order.transition("cancel") # ValueError: Cannot cancel from SHIPPED
print(order.history)
# ['confirm → CONFIRMED', 'ship → SHIPPED']
Dataclass + context manager
from contextlib import contextmanager
from dataclasses import dataclass
from typing import List
@dataclass
class Transaction:
items: List[str] = field(default_factory=list)
committed: bool = False
def add(self, item: str) -> None:
if self.committed:
raise RuntimeError("Transaction already committed")
self.items.append(item)
def commit(self) -> None:
print(f"Committing: {self.items}")
self.committed = True
def rollback(self) -> None:
print("Rolling back...")
self.items.clear()
self.committed = False
@contextmanager
def atomic(self):
"""Контекстный менеджер: commit при успехе, rollback при ошибке"""
try:
yield self
self.commit()
except Exception:
self.rollback()
raise
tx = Transaction()
with tx.atomic():
tx.add("item1")
tx.add("item2")
# Committing: ['item1', 'item2']
with tx.atomic():
tx.add("item3")
raise ValueError("Something went wrong")
# Rolling back...
# ValueError: Something went wrong
26. Best Practices
Организация
- Используйте dataclass для DTO, конфигов, value objects — не для бизнес-логики
- Выносите сложную валидацию в отдельные функции/классы, не в
__post_init__ - Группируйте dataclass в отдельном модуле (
models.py,schemas.py,dto.py) - Для публичных API библиотек используйте
repr=Falseдля чувствительных полей
Типизация
- Всегда аннотируйте типы полей — иначе dataclass не определит поле
- Используйте
Optional[T]для опциональных полей - Для сложных типов используйте
TypeAlias(3.10+) - Используйте
field(metadata=...)для дополнительной информации - Настройте mypy/pyright с
--strictдля проверки типов
Производительность
slots=Trueдля классов с тысячами экземпляров (-73% памяти)- Избегайте сложной логики в
__post_init__для hot-path - Для высоконагруженных мест — namedtuple или dataclass+slots
- pydantic ≈ 10x медленнее — не используйте для hot-path без необходимости
Наследование
- Избегайте глубокой иерархии (более 2 уровней)
kw_only=Trueдля решения проблем с порядком полей- Не забывайте
super().__post_init__()в дочерних классах - Множественное наследование — не более 2-3 dataclass-родителей
Тестирование
- Используйте
asdict/astupleдля сравнения в тестах factory_boyдля генерации тестовых dataclass (factory.DataclassFactory)hypothesisдля property-based тестирования dataclass- Проверяйте frozen: попытка изменения должна падать с
FrozenInstanceError
# Пример factory_boy с dataclass:
# pip install factory_boy
import factory
@dataclass
class User:
name: str
email: str
age: int = 0
class UserFactory(factory.Factory):
class Meta:
model = User
name = factory.Faker("name")
email = factory.Faker("email")
age = factory.Faker("random_int", min=18, max=80)
# В тестах:
def test_user():
user = UserFactory()
assert user.name != ""
assert "@" in user.email
Миграция с plain class на dataclass
// До:
class OldUser:
def __init__(self, name, email, age=0):
self.name = name
self.email = email
self.age = age
def __repr__(self):
return f"OldUser(name={self.name!r}, email={self.email!r}, age={self.age})"
def __eq__(self, other):
if not isinstance(other, OldUser):
return NotImplemented
return (self.name, self.email, self.age) == (other.name, other.email, other.age)
// После:
@dataclass
class NewUser:
name: str
email: str
age: int = 0
# + 30 строк кода убрано, + типизация, + 0 багов в __eq__
Безопасность
- Не храните секреты в
repr(используйтеrepr=False) - Валидируйте входные данные в
__post_init__ - Для данных из внешних источников — pydantic, не голый dataclass
InitVarдля временных данных (пароли, токены) — они не сохраняются
27. Глоссарий терминов dataclass
| Термин | Определение |
|---|---|
| Dataclass | Класс с декоратором @dataclass, автоматически генерирующий __init__, __repr__, __eq__ |
| Field | Поле dataclass, определённое аннотацией типа с опциональным field() |
| field() | Функция настройки поля: default, repr, compare, metadata, kw_only |
| default_factory | Фабрика для мутабельных дефолтов (list, dict, set, lambda, uuid.uuid4) |
| __post_init__ | Метод после __init__ для валидации и вычисления полей |
| Frozen | frozen=True — поля нельзя менять после создания |
| InitVar | Переменная init, не становящаяся полем экземпляра |
| ClassVar | Аннотация классового поля — игнорируется dataclass |
| KW_ONLY | Маркер: поля после него — keyword-only |
| kw_only | Параметр @dataclass: все поля keyword-only |
| Slots | slots=True — генерация __slots__ (меньше памяти + быстрее) |
| asdict | Рекурсивное преобразование dataclass → dict |
| astuple | Рекурсивное преобразование dataclass → tuple |
| replace | Копирование frozen dataclass с изменением отдельных полей |
| Metadata | Метаданные поля через field(metadata=...) |
| __dataclass_fields__ | Словарь Field-объектов для интроспекции структуры |
| Generic dataclass | Dataclass с TypeVar (Generic[T]) для типизированных контейнеров |
| DTO | Data Transfer Object — объект для передачи данных между слоями |
| Value Object | Иммутабельный объект, определяемый значениями (frozen dataclass) |
| PEP 557 | PEP, добавивший dataclasses в стандартную библиотеку (3.7) |
| PEP 681 | PEP, добавивший dataclass_transform для библиотек (3.11) |
| unsafe_hash | Принудительная генерация __hash__ для мутабельного класса |
| weakref_slot | Добавить __weakref__ в слоты (3.11+) |
| deep freeze | Глубокое замораживание: frozen + кортежи вместо списков |
| cattrs | Библиотека сериализации/десериализации dataclass ↔ dict |
| mashumaro | Быстрая библиотека сериализации с поддержкой Union |
| dataclasses-json | Библиотека с декоратором @dataclass_json (from_json/to_json) |
| dataclass_transform | Декоратор из typing для пометки кастомных декораторов как dataclass |
| MappedAsDataclass | Базовый класс SQLAlchemy для dataclass-моделей (2.0+) |
28. Чеклист dataclass
Базовая настройка
- Стоит декоратор
@dataclass - Все поля аннотированы типом
- Мутабельные дефолты через
default_factory - Порядок полей: сначала без умолчаний, потом с умолчаниями
-
__post_init__для валидации и вычисляемых полей -
__dataclass_fields__используется для интроспекции где нужно
Параметры декоратора
-
frozen=Trueдля конфигов, value objects, DTO -
order=Trueвключено для классов, требующих сортировки -
slots=Trueдля массового создания (1000+ экземпляров) -
kw_only=Trueдля решения проблем с наследованием -
repr=Falseдля чувствительных полей -
weakref_slot=Trueесли нужны слабые ссылки (3.11+)
Поля (field)
-
default_factoryдля list, dict, set, uuid, datetime -
field(compare=False)для полей, не участвующих в сравнении -
field(hash=False)для полей, исключённых из хэша -
field(repr=False)для секретов и больших полей -
field(metadata=...)для доп. информации о поле -
KW_ONLYилиkw_onlyдля keyword-only полей
InitVar / ClassVar
-
InitVarдля временных данных, не сохраняемых в экземпляре -
InitVarдля DI (передача зависимостей в post_init) -
ClassVarдля классовых полей (константы, счётчики) - InitVar проброшен в
super().__post_init__при наследовании
Наследование
-
super().__post_init__()вызывается в дочернем классе -
kw_only=Trueдля решения проблем с порядком полей - Не более 2 уровней наследования
- Множественное наследование — MRO проверен
Сериализация
-
asdict/astupleдля базовой сериализации - JSON-encoder для datetime, UUID, Decimal, bytes, set
- cattrs / dataclasses-json / mashumaro для десериализации
- Десериализация с валидацией для внешних данных
Производительность
-
slots=Trueдля массового создания (3.10+) - Нет тяжёлой логики в
__post_init__для hot-path - frozen dataclass для хэшируемых объектов (keys в dict/set)
- pydantic только там, где нужна строгая валидация (не hot-path)
Типизация и дизайн
- Generic dataclass типизированы через TypeVar
- mypy / pyright проходит без ошибок
- ABC/Protocol для полиморфизма dataclass
- factory_boy / hypothesis для тестирования
-
dataclass_transformдля кастомных декораторов библиотек (3.11+)
Гайд подготовлен для проекта qasdet.github.io • 2026