Декоратор принимает что угодно: сохраняем типы аргументов через ParamSpec
Допустим, вокруг функций приложения появился декоратор аудита. Он записывает начало вызова, затем передаёт аргументы дальше. Во время выполнения всё работает, но редактор перестаёт подсказывать параметры, а проверка типов пропускает вызов с неправильным именем аргумента. Причина часто находится в аннотации Callable[..., Any]: она разрешает произвольный вызов и стирает сведения об ответе.
Для прозрачной обёртки нужно сохранить две независимые части контракта. TypeVar описывает возвращаемое значение, ParamSpec — весь набор параметров, включая их имена и способ передачи. Обёртка получает P.args и P.kwargs и возвращает тот же R, что исходная функция. Здесь не требуется вручную перечислять возможные сигнатуры.
Связь параметров обёртки
Ниже декоратор ничего не меняет в протоколе функции. Он добавляет запись в список, а затем вызывает исходную операцию. Список позволяет проверить пример без настройки логирования. В прикладном коде вместо него может быть заранее переданная функция аудита, но связь параметров останется такой же.
from collections.abc import Callable
from functools import wraps
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
events: list[str] = []
def audited(fn: Callable[P, R]) -> Callable[P, R]:
@wraps(fn)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
events.append(fn.__name__)
return fn(*args, **kwargs)
return wrapper
@audited
def render(name: str, *, prefix: str = "Hello") -> str:
return f"{prefix}, {name}"
assert render("Ann", prefix="Hi") == "Hi, Ann"
assert events == ["render"]
print(render("Bob"))
Что проверяет mypy
На Python 3.12 этот файл запускается без внешних зависимостей. Для статической проверки установите mypy: python -m pip install mypy. Команда python -m mypy --strict audit.py должна пройти. Добавьте вызов render(name=12): проверяющий заметит, что требуется str. Добавьте render(user="Ann"): появится ошибка о неожиданном keyword. Раньше Callable[..., Any] мог скрыть оба дефекта.
functools.wraps тоже нужен, но решает другую часть задачи: переносит метаданные обёрнутой функции. ParamSpec описывает передачу параметров для статического анализа. Ни один из этих инструментов не проверяет JSON на входе и не преобразует аргументы. Если декоратор действительно добавляет обязательный параметр или меняет результат на awaitable, его контракт нужно описать отдельно, а не заставлять прозрачную схему притворяться подходящей.
Полезно проверить и порядок побочного действия. В примере запись появляется до успешного результата. Если исходная функция выбросит исключение, журнал уже содержит попытку вызова. Для аудита завершения запись должна находиться после вызова; для гарантированной очистки используется finally. Аннотация не решает эту инженерную задачу, зато позволяет менять реализацию, сохраняя видимый интерфейс.
Применять ParamSpec стоит там, где обёртка пересылает параметры дальше: измерение времени, трассировка, повтор, проверка доступа. При ревью смотрите не только на отсутствие ошибок mypy, но и на смысл дополнительных действий. Сохранив сигнатуру, проще увидеть действительно новое поведение декоратора и написать для него отдельную проверку.
Следующий шаг — перенести этот приём из отдельного файла в поддерживаемое приложение. В курсе «Python для продвинутых: ООП, типизация и тестирование» структура пакетов, объектная модель, mypy и pytest соединяются в типизированном трекере задач с хранением данных и логированием. Можно отработать связь между интерфейсом, реализацией и регрессионным тестом, а затем применить её к своему проекту.
Посмотреть программу и практику курса.
Документация: официальный источник по теме.
Другие задачи по теме: Типы, тесты и поведение программы.
Продолжить на практике
- Python для продвинутых: ООП, типизация и тестирование ↗
Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.
