Kodokon kodokon.com

Аннотации типов и dataclasses: серьёзная типизация современного Python

Разберись, что аннотации на самом деле делают во время выполнения, и выжми максимум из dataclasses с frozen, slots и default_factory.

10 мин · 3 вопросов

Открыть этот урок в Kodokon

Аннотации типов - это метаданные, а не ограничения: интерпретатор хранит их в __annotations__ и никогда не проверяет во время выполнения. Проверка - задача статического анализатора вроде mypy или pyright, ещё до запуска. Прямое следствие: программа с неправильными типами выполнится без единой жалобы - и так задумано. Постепенная типизация позволяет размечать существующую кодовую базу понемногу, ничего не ломая.

PYTHON
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 делает все аннотации ленивыми (они хранятся как строки), что позволяет ссылаться на классы, объявленные ниже, и снижает стоимость импорта.

PYTHON
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])))
TypeVar гарантирует, что dedupe(list[int]) действительно отдаёт int.
PYTHON
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"]))
Point неизменяем, хешируем и не имеет собственного __dict__ у экземпляров.

Три параметра превращают @dataclass в инструмент промышленного уровня. frozen=True делает экземпляры неизменяемыми и, вместе со сгенерированным сравнением, хешируемыми - их можно использовать как ключи словаря. slots=True (Python 3.10+) убирает __dict__ у каждого экземпляра: меньше памяти, быстрее доступ к атрибутам, а любая неожиданная запись атрибута падает с ошибкой. Наконец, kw_only=True заставляет вызывать конструктор с именованными аргументами. В __post_init__ место для инвариантов и вычисляемых полей. Для простого неизменяемого набора данных NamedTuple по-прежнему уместен; как только появляется поведение или валидация, выбирай dataclass.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Что произойдёт во время выполнения при вызове add('py', 'thon'), если функция аннотирована как (a: int, b: int) -> int?
    • Python сразу вызовет TypeError
    • Вызов выполнится нормально: аннотации никогда не проверяются во время выполнения
    • Python выдаст RuntimeWarning, а затем выполнит вызов
    • Модуль откажется импортироваться
  2. Почему items: list[str] = [] запрещено как значение по умолчанию для поля dataclass?
    • Потому что list[str] - недопустимая аннотация для поля
    • Потому что dataclasses допускают только поля без значения по умолчанию
    • Потому что это изменяемое значение по умолчанию было бы общим для всех экземпляров: dataclass вызывает ValueError и требует field(default_factory=list)
    • Потому что значения по умолчанию нужно задавать в __post_init__
  3. Что frozen=True делает с dataclass?
    • Экземпляры становятся неизменяемыми и, благодаря сгенерированному сравнению, хешируемыми - их можно использовать как ключи словаря
    • От класса больше нельзя наследоваться
    • Поля становятся общими атрибутами класса
    • Аннотации проверяются при каждом присваивании