Декоратор @dataclass, поля, frozen, order
Dataclass автоматически генерирует
__init__,__repr__,__eq__— убирает boilerplate для классов-хранилищ данных.
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)@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 автоматически делает объект хэшируемым
d = {v: "origin"} # OK — можно использовать как ключ словаря!
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] # FrozenInstanceErrororder=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 экономии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()| Параметр | Тип | Значение |
|---|---|---|
default | Any | Скалярное дефолтное значение |
default_factory | Callable | Функция для изменяемых дефолтов |
init | bool | Включать в __init__ (default: True) |
repr | bool | Включать в __repr__ (default: True) |
compare | bool | Включать в __eq__, __lt__ и т.д. (default: True) |
hash | bool/None | Включать в __hash__ (default: None — следует compare) |
metadata | Mapping | Произвольные метаданные для сторонних библиотек |
kw_only | bool | Только keyword (Python 3.10+) |
__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!__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"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 в порядке MROdataclasses.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 — новый объект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')| Вариант | Плюсы | Минусы |
|---|---|---|
@dataclass | Стандартная библиотека, гибкий | Нет валидации по умолчанию |
NamedTuple | Неизменяемый, лёгкий, tuple-совместимый | Нельзя наследовать с новыми полями |
TypedDict | Для словарей, хорошо с JSON | Не объект, нет методов |
Pydantic BaseModel | Валидация, сериализация, FastAPI | Зависимость, медленнее |
attrs | Больше возможностей чем 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, просто с типами
passfrom 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 dfrom 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()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 (дубликат убран)@dataclass для Rectangle с полями width, height. Добавьте вычисляемые свойства area и perimeter через @property. Используйте frozen=True и order=True.default=[] вызывает ValueError, а default_factory=list — нет? Что произошло бы если бы дефолтный список был бы общим для всех экземпляров?@dataclass Config загружающий значения из переменных окружения через __post_init__. Поля: host: str, port: int, debug: bool.dataclasses.replace(), напишите функцию update_user(user: User, **changes) -> User возвращающую новый объект с изменёнными полями.@dataclass(slots=True) vs обычный @dataclass при создании 1 миллиона объектов через timeit.@dataclass Matrix с полем data: list[list[float]]. Реализуйте __add__ для сложения матриц и __matmul__ (@) для умножения.Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.