Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Dataclasses
dataclasses

Dataclasses

Декоратор @dataclass, поля, frozen, order

Открыть лабораториюv0.6.2Запускается локально из публичного репозитория

Dataclasses в Python

  • Маршрут: Инженерная основа · тема 3 из 8
  • Навыки: M2, M4, S2
  • До урока: oop, python_testing.
  • Результат: типизированная модель данных с явными инвариантами и корректными правилами сравнения.
  • Основа: поля, значения по умолчанию и __post_init__. Работа в эксплуатации: валидация, сериализация и выбор между dataclass/TypedDict/Pydantic. Углубление: frozen, хэширование, слоты и граничные случаи наследования.
  • Подтверждение: типизированная модель, тесты инвариантов и сравнение вариантов проектирования.

@dataclass генерирует методы по объявленным полям. Это сокращает повторяющийся код для моделей данных, но не заменяет решение о мутабельности, валидации и границе сериализации.

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

from dataclasses import dataclass @dataclass class Point: x: float y: float z: float = 0.0 # поле с дефолтным значением p1 = Point(1.0, 2.0) p2 = Point(x=3.0, y=4.0, z=5.0) print(p1) # Point(x=1.0, y=2.0, z=0.0) print(p1 == Point(1.0, 2.0)) # True — __eq__ сравнивает все поля

#Что генерируется автоматически

@dataclass class User: name: str email: str role: str = "user" # Генерируется: # __init__(self, name: str, email: str, role: str = "user") # __repr__: "User(name='Alice', email='a@b.com', role='user')" # __eq__: сравнивает (name, email, role) пословно # НЕ генерируется по умолчанию: # __hash__ (если eq=True, то hash=None — объект нехэшируемый) # __lt__, __le__, __gt__, __ge__ (нужен order=True)

#2. Параметры декоратора @dataclass

@dataclass( init=True, # генерировать __init__ repr=True, # генерировать __repr__ eq=True, # генерировать __eq__ order=False, # генерировать __lt__, __le__, __gt__, __ge__ unsafe_hash=False, # принудительно генерировать __hash__ frozen=False, # сделать объект неизменяемым slots=False, # использовать __slots__ (Python 3.10+) kw_only=False, # все поля только keyword-only (Python 3.10+) match_args=True, # генерировать __match_args__ для pattern matching ) class MyClass: ...

#frozen=True — неизменяемые объекты

@dataclass(frozen=True) class Vector: x: float y: float v = Vector(1.0, 2.0) # v.x = 3.0 # FrozenInstanceError! # При frozen=True и eq=True dataclass генерирует __hash__ d = {v: "origin"} # OK, потому что поля Vector хешируемы s = {v} # OK — можно добавить в set! # Но вложенные изменяемые объекты всё ещё меняются: @dataclass(frozen=True) class Bag: items: list bag = Bag(items=[1, 2, 3]) bag.items.append(4) # OK! list — изменяемый # bag.items = [5, 6] # FrozenInstanceError # hash(bag) # TypeError: поле list нехешируемо

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

#order=True — сравнение объектов

@dataclass(order=True) class Version: major: int minor: int patch: int v1 = Version(1, 0, 0) v2 = Version(2, 0, 0) v3 = Version(1, 5, 0) print(v1 < v2) # True — сравниваются лексикографически (major, minor, patch) print(v3 > v1) # True versions = [v2, v3, v1] print(sorted(versions)) # [Version(1,0,0), Version(1,5,0), Version(2,0,0)]

#slots=True — экономия памяти (Python 3.10+)

import sys @dataclass class WithDict: x: int y: int @dataclass(slots=True) class WithSlots: x: int y: int d = WithDict(1, 2) s = WithSlots(1, 2) print(sys.getsizeof(d) + sys.getsizeof(d.__dict__)) # ~152 bytes print(sys.getsizeof(s)) # ~56 bytes — нет __dict__! # При миллионе объектов: ~100 MB экономии

#3. Поля с field()

from dataclasses import dataclass, field from typing import ClassVar import datetime @dataclass class Order: id: int items: list = field(default_factory=list) # изменяемый дефолт! created_at: datetime.datetime = field( default_factory=datetime.datetime.now ) _internal: str = field(init=False, default="private") notes: str = field(default="", repr=False) # не в __repr__ priority: int = field(default=0, compare=False) # не в __eq__ # Переменная класса — не поле датакласса counter: ClassVar[int] = 0 o = Order(id=1) print(o.items) # [] — новый список каждый раз print(o._internal) # "private" — не в __init__ # Проблема: Нельзя: изменяемый дефолт без field() @dataclass class Bad: items: list = [] # ValueError! Используйте field(default_factory=list)

#Параметры field()

ПараметрТипЗначение
defaultAnyСкалярное дефолтное значение
default_factoryCallableФункция для изменяемых дефолтов
initboolВключать в __init__ (default: True)
reprboolВключать в __repr__ (default: True)
compareboolВключать в __eq__, __lt__ и т.д. (default: True)
hashbool/NoneВключать в __hash__ (default: None — следует compare)
metadataMappingПроизвольные метаданные для сторонних библиотек
kw_onlyboolТолько keyword (Python 3.10+)

#4. __post_init__ — пост-инициализация

Вызывается автоматически после __init__:

from dataclasses import dataclass, field import re @dataclass class EmailAddress: raw: str local: str = field(init=False) domain: str = field(init=False) def __post_init__(self): # Валидация pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' if not re.match(pattern, self.raw): raise ValueError(f"Невалидный email: {self.raw}") # Вычисление производных полей self.local, self.domain = self.raw.split("@") e = EmailAddress("alice@example.com") print(e.local) # "alice" print(e.domain) # "example.com" EmailAddress("not-an-email") # ValueError!

#Входные параметры через InitVar

InitVar[T] добавляет параметр в сгенерированный __init__ и передаёт его в __post_init__, но не сохраняет как поле объекта. Это подходит для секрета, сырого ввода или зависимости, которая нужна только во время построения.

from dataclasses import InitVar, dataclass, field @dataclass class User: name: str password: InitVar[str] password_hash: str = field(init=False) def __post_init__(self, password: str) -> None: self.password_hash = hash_password(password)

#Инициализация frozen-объекта

Сгенерированный __setattr__ запрещает обычное присваивание полей в frozen=True dataclass. Внутри __post_init__ вычисляемое поле можно один раз задать через object.__setattr__:

@dataclass(frozen=True) class Rectangle: width: float height: float area: float = field(init=False) def __post_init__(self) -> None: object.__setattr__(self, "area", self.width * self.height)

Тот же вызов технически может изменить объект и после создания, поэтому frozen=True защищает от обычного присваивания, но не создаёт жёсткую границу безопасности и не замораживает вложенные списки или словари.

#__post_init__ с наследованием

from dataclasses import dataclass @dataclass class Animal: name: str sound: str def __post_init__(self): self.name = self.name.capitalize() @dataclass class Dog(Animal): breed: str def __post_init__(self): super().__post_init__() # не забыть вызвать родительский! self.breed = self.breed.title() d = Dog("rex", "woof", "golden retriever") print(d.name) # "Rex" print(d.breed) # "Golden Retriever"

#5. Наследование датаклассов

from dataclasses import dataclass @dataclass class Base: id: int created_at: str = "2024-01-01" @dataclass class Extended(Base): name: str = "" # поля с дефолтом после полей с дефолтом — OK # Проблема: поля без дефолта после полей с дефолтом @dataclass class Base: id: int = 0 # с дефолтом @dataclass class Child(Base): name: str # без дефолта — TypeError! # TypeError: non-default argument 'name' follows default argument # Решение 1: kw_only (Python 3.10+) @dataclass class Child(Base): name: str = field(kw_only=True) # Решение 2: дать дефолт @dataclass class Child(Base): name: str = "" # MRO при множественном наследовании @dataclass class A: x: int @dataclass class B(A): y: int @dataclass class C(A): z: int @dataclass class D(B, C): pass # __init__ включит x, y, z в порядке MRO

#6. dataclasses.asdict() и dataclasses.astuple()

from dataclasses import dataclass, asdict, astuple, fields, replace @dataclass class Address: city: str country: str @dataclass class Person: name: str age: int address: Address p = Person("Alice", 30, Address("Moscow", "Russia")) # Рекурсивная конвертация в dict d = asdict(p) print(d) # {"name": "Alice", "age": 30, "address": {"city": "Moscow", "country": "Russia"}} # Рекурсивная конвертация в tuple t = astuple(p) print(t) # ("Alice", 30, ("Moscow", "Russia")) # Получить список полей for f in fields(p): print(f.name, f.type, getattr(p, f.name)) # replace() — создать копию с изменёнными полями (как frozen copy) p2 = replace(p, age=31) print(p2) # Person(name='Alice', age=31, address=...) print(p is p2) # False — новый объект

asdict() не является JSON-сериализатором: datetime, Path, set и пользовательские типы нужно преобразовывать по явному контракту. Не используйте широкий json.dumps(..., default=str) как универсальное решение — он может молча скрыть неподдерживаемый тип.


#7. Датаклассы и протоколы типизации

from dataclasses import dataclass from typing import Protocol, runtime_checkable @runtime_checkable class HasId(Protocol): id: int @dataclass class User: id: int name: str @dataclass class Product: id: int title: str # Работает со всеми датаклассами имеющими поле id def find_by_id(items: list[HasId], id: int) -> HasId | None: return next((item for item in items if item.id == id), None) users = [User(1, "Alice"), User(2, "Bob")] products = [Product(10, "Widget")] print(find_by_id(users, 2)) # User(id=2, name='Bob') print(find_by_id(products, 10)) # Product(id=10, title='Widget')

#8. Сравнение с альтернативами

ВариантПлюсыМинусы
@dataclassСтандартная библиотека, гибкийНет валидации по умолчанию
NamedTupleНеизменяемый, лёгкий, tuple-совместимыйНельзя наследовать с новыми полями
TypedDictДля словарей, хорошо с JSONНе объект, нет методов
Pydantic BaseModelВалидация, сериализация, FastAPIЗависимость, медленнее
attrsБольше возможностей чем dataclassЗависимость

attrs появился раньше dataclasses и предоставляет встроенные validators, converters и дополнительные hooks жизненного цикла. Dataclass входит в стандартную библиотеку и оставляет такую логику обычным методам или внешним библиотекам. Выбор зависит от нужного контракта, а не от размера примера.

from typing import NamedTuple, TypedDict # NamedTuple — неизменяемый, tuple под капотом class Point(NamedTuple): x: float y: float label: str = "" p = Point(1.0, 2.0) x, y = p # можно распаковать как tuple print(p[0]) # 1.0 — доступ по индексу # TypedDict — типизированный словарь (для JSON-структур) class Config(TypedDict): host: str port: int debug: bool def connect(config: Config) -> None: # config — обычный dict, просто с типами pass

#9. Практические паттерны

#DTO (Data Transfer Object) для API

from dataclasses import dataclass, asdict from typing import Optional import datetime @dataclass class UserCreateRequest: name: str email: str password: str def __post_init__(self): if len(self.password) < 8: raise ValueError("Пароль слишком короткий") if "@" not in self.email: raise ValueError("Неверный email") @dataclass class UserResponse: id: int name: str email: str created_at: datetime.datetime # Пароль не включён — это DTO для ответа! def to_json(self) -> dict: d = asdict(self) d["created_at"] = self.created_at.isoformat() return d

#Конфигурация

from dataclasses import dataclass, field import os @dataclass class AppConfig: database_url: str = field( default_factory=lambda: os.getenv("DATABASE_URL", "sqlite:///dev.db") ) redis_url: str = field( default_factory=lambda: os.getenv("REDIS_URL", "redis://localhost:6379") ) debug: bool = field( default_factory=lambda: os.getenv("DEBUG", "0") == "1" ) max_connections: int = 10 allowed_origins: list = field(default_factory=list) config = AppConfig()

#Иммутабельные Value Objects

from dataclasses import dataclass from functools import total_ordering @dataclass(frozen=True, order=True) class Money: amount: int # в копейках (избегаем float!) currency: str def __post_init__(self): if self.amount < 0: raise ValueError("Сумма не может быть отрицательной") def __add__(self, other: "Money") -> "Money": if self.currency != other.currency: raise ValueError("Нельзя складывать разные валюты") return Money(self.amount + other.amount, self.currency) def __str__(self) -> str: return f"{self.amount / 100:.2f} {self.currency}" price = Money(9990, "RUB") tax = Money(990, "RUB") total = price + tax print(total) # 109.80 RUB # Хэшируемый — можно в set и dict prices = {Money(100, "RUB"), Money(200, "RUB"), Money(100, "RUB")} print(len(prices)) # 2 (дубликат убран)

#Практика: от пробы к рабочему решению

#Шаг 1. Соберите минимальный пример

Используйте dataclass для модели данных с ясными полями, а не как замену любой бизнес-модели. Результат основы — неизменяемая модель с безопасными значениями по умолчанию и проверяемым инвариантом.

#Шаг 2. Проверьте поведение

from dataclasses import dataclass @dataclass(frozen=True, slots=True) class Task: task_id: str tags: tuple[str, ...] = () def __post_init__(self) -> None: if not self.task_id.strip(): raise ValueError("task_id не должен быть пустым") first = Task("T-1") second = Task("T-2") assert first.tags == second.tags == () assert isinstance(hash(first), int)

#Шаг 3. Доведите решение до рабочего сценария

Практика с подсказками: перенесите словарь входной задачи в dataclass. До реализации решите, где проходит граница преобразования из JSON, какие поля могут отсутствовать и должна ли модель быть хэшируемой.

#Шаг 4. Объясните внутренний механизм

Сравните обычный dataclass, slots=True и ручной класс на фиксированном наборе объектов. Учитывайте наследование, weakref, наличие __dict__ и версию CPython; не делайте вывода о памяти только по одному объекту.

#Упражнения

  1. Создайте @dataclass для Rectangle с полями width, height. Добавьте вычисляемые свойства area и perimeter через @property. Используйте frozen=True и order=True.
  2. Почему default=[] вызывает ValueError, а default_factory=list — нет? Что произошло бы если бы дефолтный список был бы общим для всех экземпляров?
  3. Реализуйте @dataclass Config загружающий значения из переменных окружения через __post_init__. Поля: host: str, port: int, debug: bool.
  4. Используя dataclasses.replace(), напишите функцию update_user(user: User, **changes) -> User возвращающую новый объект с изменёнными полями.
  5. Сравните производительность @dataclass(slots=True) vs обычный @dataclass при создании 1 миллиона объектов через timeit.
  6. Напишите @dataclass Matrix с полем data: list[list[float]]. Реализуйте __add__ для сложения матриц и __matmul__ (@) для умножения.

#Самопроверка

  • Rectangle отклоняет неположительные размеры и остаётся хэшируемым только потому, что все его поля хэшируемы; сортировка имеет явно выбранный смысл.
  • Для default_factory создайте два экземпляра и докажите a.items is not b.items.
  • Config проверяется с отсутствующими, корректными и некорректными переменными окружения; чтение окружения через @classmethod часто яснее скрытой работы в __post_init__.
  • Бенчмарк запускается сериями, показывает распределение, версию Python и не обещает фиксированного ускорения.
  • Matrix проверяет прямоугольность и совместимость размеров, не меняет входы и имеет тесты на 1×1, сложение и несовместимые матрицы.

#Практическая лаборатория

Закрепите тему в лаборатории «Неизменяемая модель развёртывания». Вы отделите JSON-представление от модели, нормализуете время в UTC и проверите, что вложенные метки действительно неизменяемы и участвуют в хэше.

git clone --branch v0.6.2 --depth 1 https://gitlab.potapov.me/courses/python-labs.git cd python-labs uv sync --group test uv run --group test pytest dataclasses/tests

Далее: Замыкания и декораторы