Разберись, что аннотации на самом деле делают во время выполнения, и выжми максимум из dataclasses с frozen, slots и default_factory.
Открыть этот урок в KodokonАннотации типов - это метаданные, а не ограничения: интерпретатор хранит их в __annotations__ и никогда не проверяет во время выполнения. Проверка - задача статического анализатора вроде mypy или pyright, ещё до запуска. Прямое следствие: программа с неправильными типами выполнится без единой жалобы - и так задумано. Постепенная типизация позволяет размечать существующую кодовую базу понемногу, ничего не ломая.
from typing import get_type_hints
def add(a: int, b: int) -> int:
return a + b
print(add("py", "thon"))
print(add.__annotations__)
print(get_type_hints(add))Начиная с Python 3.9 встроенные типы сами по себе являются обобщёнными: пиши list[int] или dict[str, float], не импортируя typing.List. Объединения записываются как int | None начиная с версии 3.10. Чтобы связать тип на входе с типом на выходе, используй TypeVar - в Python 3.12 есть даже компактный синтаксис def first[T](items: list[T]) -> T. Момент, о котором часто забывают: from __future__ import annotations делает все аннотации ленивыми (они хранятся как строки), что позволяет ссылаться на классы, объявленные ниже, и снижает стоимость импорта.
from collections.abc import Iterable, Iterator
from typing import TypeVar
T = TypeVar("T")
def dedupe(items: Iterable[T]) -> Iterator[T]:
seen: set[T] = set()
for item in items:
if item not in seen:
seen.add(item)
yield item
print(list(dedupe([3, 1, 3, 2, 1])))from dataclasses import dataclass, field
@dataclass(frozen=True, slots=True)
class Point:
x: float
y: float
@dataclass
class Basket:
items: list[str] = field(default_factory=list)
total: int = 0
def __post_init__(self) -> None:
self.total = len(self.items)
point = Point(1.0, 2.0)
print(point, hash(point))
print(Basket(["book", "pen"]))Три параметра превращают @dataclass в инструмент промышленного уровня. frozen=True делает экземпляры неизменяемыми и, вместе со сгенерированным сравнением, хешируемыми - их можно использовать как ключи словаря. slots=True (Python 3.10+) убирает __dict__ у каждого экземпляра: меньше памяти, быстрее доступ к атрибутам, а любая неожиданная запись атрибута падает с ошибкой. Наконец, kw_only=True заставляет вызывать конструктор с именованными аргументами. В __post_init__ место для инвариантов и вычисляемых полей. Для простого неизменяемого набора данных NamedTuple по-прежнему уместен; как только появляется поведение или валидация, выбирай dataclass.
add('py', 'thon'), если функция аннотирована как (a: int, b: int) -> int?items: list[str] = [] запрещено как значение по умолчанию для поля dataclass?list[str] - недопустимая аннотация для поляfrozen=True делает с dataclass?