overload в Python: тип результата зависит от аргумента
Функция находит запись по номеру. Обычно отсутствие записи возвращает None, но режим required должен выдавать ошибку. Общий тип результата Record | None заставляет проверять None даже там, где required=True уже гарантирует запись.
Связать флаг и обещание функции
Перегрузки typing.overload описывают несколько вариантов вызова для статического анализатора. Реализация при этом одна. Сигнатура с Literal[True] обещает Record, вариант с False — Record | None. У вызывающего кода появляется точная информация, соответствующая выбранному режиму.
Функция обязана выполнить это обещание во время работы. Если запись отсутствует и required=True, она поднимает LookupError. Возврат None в этой ветке нарушил бы аннотацию, даже если отдельный вызов в тесте не упал. Поэтому проверяем не только найденную запись, но и обе ветки отсутствия.
Оставить вариант для переменного bool
Флаг может быть вычислен во время работы. В таком случае анализатор знает только bool и должен получить объединённый результат. Третья перегрузка покрывает этот вызов. Порядок идёт от более узких вариантов к общему, иначе общий вариант может скрыть полезную точность.
Пример самодостаточен. Его можно выполнить Python и отдельно проверить mypy. Исполнение проверяет ветки функции; статическая проверка отвечает за согласованность описанных вариантов. Ни одна из этих проверок не подменяет другую.
from dataclasses import dataclass
from typing import Literal, overload
@dataclass(frozen=True)
class Record:
name: str
@overload
def find(key: int, *, required: Literal[True]) -> Record: ...
@overload
def find(key: int, *, required: Literal[False] = False) -> Record | None: ...
@overload
def find(key: int, *, required: bool) -> Record | None: ...
def find(key: int, *, required: bool = False) -> Record | None:
record = {1: Record("alpha")}.get(key)
if record is None and required:
raise LookupError(key)
return record
assert find(1, required=True).name == "alpha"
assert find(2) is None
try:
find(2, required=True)
except LookupError:
pass
else:
raise AssertionError("required record missing")
print("lookup modes checked")Когда перегрузки усложняют API
Если вариантов становится слишком много, потребителю трудно помнить связь флагов. Иногда две функции find и get_required понятнее одной перегруженной. overload полезен там, где варианты действительно выражают один механизм и часто используются вместе. Количество сигнатур не должно компенсировать непонятные имена аргументов.
Не пишите несколько обычных def с одним именем: Python сохранит только последнюю. Декораторы overload предназначены для типового описания перед реализацией. Во время выполнения вызывается итоговая функция, а не подходящая по типам заглушка.
Потренировать точные аннотации
В открытом уроке type hints и mypy начните с типов аргументов и результата. Затем проверьте описанный поиск с двумя режимами. Посмотрите программу курса: Optional, Union и Literal дают основу для таких сигнатур.
Продолжить на практике
- Python для продвинутых: ООП, типизация и тестирование ↗
Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.
