dataclasses и NamedTuple

⏱ 35 мин · 🧩 заданий: 5

Проблема: шаблонный код

Класс для хранения данных на обычном Python требует много однообразного кода:

class Product:
    def __init__(self, name, price, qty=0):
        self.name = name
        self.price = price
        self.qty = qty

    def __repr__(self):
        return f"Product(name={self.name!r}, price={self.price!r}, qty={self.qty!r})"

    def __eq__(self, other):
        if not isinstance(other, Product):
            return NotImplemented
        return (self.name, self.price, self.qty) == (other.name, other.price, other.qty)

@dataclass пишет всё это за вас по аннотациям полей:

from dataclasses import dataclass

@dataclass
class Product:
    name: str
    price: float
    qty: int = 0

    def total(self):
        return self.price * self.qty

p = Product("Кофе", 199.0, 3)
print(p)
print(p.total(), p == Product("Кофе", 199.0, 3))
p.qty += 1                     # обычный изменяемый объект
print(p)

Аннотации обязательны

Поле dataclass — это атрибут класса с аннотацией типа. Тип не проверяется во время выполнения (Product(1, "x") сработает), он нужен для генерации и для mypy/IDE.

Изменяемые значения по умолчанию

from dataclasses import dataclass

@dataclass
class Order:
    items: list = []
from dataclasses import dataclass, field

@dataclass
class Order:
    id: int
    items: list[str] = field(default_factory=list)   # фабрика вызывается для каждого объекта
    meta: dict = field(default_factory=dict, repr=False)   # не показывать в repr

a, b = Order(1), Order(2)
a.items.append("книга")
print(a, b)

field() настраивает поле: default, default_factory, repr=False, compare=False (не участвует в ==), init=False (не попадает в __init__).

post_init: проверки и вычисляемые поля

from dataclasses import dataclass, field

@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)          # вычисляется, а не передаётся

    def __post_init__(self):                  # вызывается после сгенерированного __init__
        if self.width <= 0 or self.height <= 0:
            raise ValueError("Стороны должны быть положительными")
        self.area = self.width * self.height

print(Rectangle(3, 4))
try:
    Rectangle(0, 5)
except ValueError as e:
    print("ValueError:", e)

Параметры декоратора

Параметр Что делает
frozen=True запрещает присваивание полям → объект неизменяемый и хешируемый
order=True генерирует <, <=, >, >= (сравнение как кортежей полей)
slots=True использует __slots__: меньше памяти, нельзя добавлять атрибуты (3.10+)
kw_only=True все поля только именованные (3.10+)
eq=False не генерировать __eq__ (сравнение по идентичности)
from dataclasses import dataclass

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

v = Version(1, 10)
print(sorted([Version(1, 9, 3), v, Version(0, 5)]))
print(v > Version(1, 9, 99), {v, Version(1, 10, 0)})
try:
    v.major = 2
except Exception as e:
    print(type(e).__name__, e)

Утилиты: asdict, replace, fields

from dataclasses import dataclass, asdict, astuple, replace, fields

@dataclass(frozen=True)
class User:
    name: str
    email: str
    active: bool = True

u = User("Анна", "anna@x.ru")
print(asdict(u))                       # в словарь (рекурсивно) — удобно для JSON
print(astuple(u))
u2 = replace(u, active=False)          # копия с изменёнными полями — так «меняют» frozen-объекты
print(u2, u2 is u)
print([f.name for f in fields(User)])

Наследование dataclass

from dataclasses import dataclass

@dataclass
class Animal:
    name: str
    age: int = 0

@dataclass
class Dog(Animal):
    breed: str = "дворняга"      # после полей со значением по умолчанию — тоже со значением

print(Dog("Шарик", 3, "лайка"))

Порядок полей

Поля без значения по умолчанию не могут идти после полей со значением — как и параметры функции. При наследовании это часто мешает; решение — kw_only=True.

NamedTuple

typing.NamedTuple — неизменяемый кортеж с именованными полями. Он лёгкий, распаковывается, индексируется:

from typing import NamedTuple
from collections import namedtuple

class Point(NamedTuple):
    x: float
    y: float = 0.0

    def dist(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5

p = Point(3, 4)
print(p, p.x, p[1], p.dist())
x, y = p                             # распаковка, как у кортежа
print(x, y, p._asdict(), p._replace(x=0))
print(p == (3, 4))                   # сравнивается с обычным кортежем!

Color = namedtuple("Color", "r g b")  # старый функциональный синтаксис
print(Color(255, 0, 0))

TypedDict

Когда данные остаются словарём (JSON из API), но хочется подсказок типов — TypedDict. Это только аннотация для mypy, во время выполнения это обычный dict:

from typing import TypedDict

class UserJSON(TypedDict):
    id: int
    name: str

data: UserJSON = {"id": 1, "name": "Анна"}
print(data, type(data).__name__)

Что выбрать

Инструмент Изменяемость Когда использовать
dict да разнородные данные, JSON, неизвестный набор ключей
TypedDict да словарь с известной структурой и проверкой mypy
NamedTuple нет маленькие неизменяемые записи, возврат нескольких значений из функции
@dataclass настраивается доменные объекты, конфиги, DTO — выбор по умолчанию
Pydantic BaseModel да данные извне: проверка и преобразование типов во время выполнения (FastAPI)
обычный класс да сложное поведение, нетривиальный __init__

Вопрос на собеседовании

«Почему dataclass с eq=True не хешируемый?» — объекты с одинаковыми полями равны, значит должны иметь одинаковый хеш. Но изменяемый объект может поменять поля после того, как попал в словарь, и сломать его. Поэтому dataclass ставит __hash__ = None, если не указан frozen=True (или unsafe_hash=True).

Итог

  • @dataclass генерирует __init__, __repr__, __eq__ по аннотированным полям.
  • Изменяемые значения по умолчанию — через field(default_factory=...).
  • __post_init__ — для проверок и вычисляемых полей.
  • frozen=True — неизменяемость и хешируемость; order=True — сравнения; slots=True — экономия памяти.
  • NamedTuple — неизменяемый кортеж с именами; TypedDict — аннотированный словарь; Pydantic — проверка данных во время выполнения.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Опишите dataclass Product с полями name: str, price: float и tags: list[str] (по умолчанию — новый пустой список у каждого объекта). Добавьте метод with_vat(), возвращающий цену с НДС 20% (price * 1.2, округлить до 2 знаков).

Какие тесты проверяют код (4)
p = Product("Кофе", 100); assert p.tags == [] and p.with_vat() == 120.0
a, b = Product("a", 1), Product("b", 2); a.tags.append("x"); assert b.tags == [], "У каждого объекта свой список — нужен field(default_factory=list)"
assert Product("Кофе", 100) == Product("Кофе", 100), "dataclass генерирует __eq__"
assert repr(Product("Кофе", 99.5)) == "Product(name=\x27Кофе\x27, price=99.5, tags=[])"
Задание 2 Программа на Python

Опишите неизменяемый dataclass Point(x: float, y: float) с проверкой в __post_init__: координаты не могут быть отрицательными (ValueError). Объекты должны быть хешируемыми, чтобы их можно было класть в множество.

Какие тесты проверяют код (3)
assert len({Point(1, 2), Point(1, 2), Point(3, 4)}) == 2
p = Point(1, 2)
try:
    p.x = 10
    ok = False
except AttributeError:
    ok = True
assert ok, "Объект должен быть неизменяемым (frozen=True)"

try:
    Point(-1, 0)
    ok = False
except ValueError:
    ok = True
assert ok, "Отрицательная координата должна давать ValueError"

Задание 3 Один вариант ответа

Что произойдёт?

from dataclasses import dataclass

@dataclass
class Cart:
    items: list = []
Задание 4 Заполнить таблицу

Что генерирует @dataclass с параметрами по умолчанию?

генерируетне генерирует
__init__
__repr__
__eq__
__lt__ и другие сравнения порядка
__hash__ (объект хешируемый)
Отметьте ячейку — «+», пустая — «−».
Задание 5 Один вариант ответа

Чем NamedTuple отличается от @dataclass?

Войдите, чтобы отмечать пройденные уроки.