dataclasses и NamedTuple
Проблема: шаблонный код
Класс для хранения данных на обычном 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
Опишите 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=[])"
from dataclasses import dataclass, field
@dataclass
class Product:
name: str
price: float
tags: list[str] = field(default_factory=list)
def with_vat(self):
return round(self.price * 1.2, 2)
Опишите неизменяемый 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"
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
def __post_init__(self):
if self.x < 0 or self.y < 0:
raise ValueError("Координаты не могут быть отрицательными")
Что произойдёт?
from dataclasses import dataclass
@dataclass
class Cart:
items: list = []
Что генерирует @dataclass с параметрами по умолчанию?
| генерирует | не генерирует | |
|---|---|---|
__init__ |
||
__repr__ |
||
__eq__ |
||
__lt__ и другие сравнения порядка |
||
__hash__ (объект хешируемый) |
Чем NamedTuple отличается от @dataclass?
Войдите, чтобы отмечать пройденные уроки.