Декораторы — штука мощная, но для типизации долгое время были настоящим кошмаром. Кажется логичным сказать: «декоратор принимает функцию и возвращает такую же функцию». На практике mypy отвечает ошибками, потому что wrapper теряет исходную сигнатуру.
Вот как обычно пытаются сделать (и почему это ломается):
from functools import wraps
from typing import Any, Callable, TypeVar
F = TypeVar("F", bound=Callable[..., Any])
def info(func: F) -> F:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
print(func.__name__)
return func(*args, **kwargs)
return wrapper # mypy: incompatible return type
Типизатор видит, что
wrapper — это уже другая функция с Callable[..., Any], а не тот же F.Правильное современное решение —
ParamSpec, который сохраняет параметры исходной функции:
from functools import wraps
from typing import Callable, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def info(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(func.__name__)
print(func.__doc__)
return func(*args, **kwargs)
return wrapper
Теперь сигнатура полностью «протаскивается» через декоратор и mypy счастлив.
А в Python 3.12+ это стало ещё красивее благодаря PEP 695:
from functools import wraps
from typing import Callable
def info[**P, R](func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(func.__name__)
print(func.__doc__)
return func(*args, **kwargs)
return wrapper
Без лишних объявлений, всё читается почти как generics в статически типизированных языках.
➡️ Подробная статья
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека питониста
#буст