Как использовать декораторы в Python: аннотации типов и примеры

Сегодня разбираем материал исходного автора о теме «Аннотации типов для декораторов в Python». Практический разбор с шагами и примерами, который можно быстро применить в своей работе.


Декораторы могут вызывать затруднения у начинающих пользователей Python, поскольку это функции, которые принимают другие функции и добавляют им новые возможности, не изменяя их изначального поведения

Аннотации типов для декораторов в Python: ключевой визуальный блок

Функции в Python ведут себя как обычные данные. Их можно передавать в другие функции или возвращать из них, как строки или целые числа

Если вы начали использовать аннотации типов, вероятно, вам захочется применить их и к собственным декораторам. Ранее это представляло определённые трудности, и мне пришлось глубоко исследовать этот вопрос.

Давайте разберёмся, как добавить аннотации типов к декоратору!

Неправильный способ добавить аннотации типов к декоратору

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

Вот пример:

from functools import wraps
from typing import Any, Callable, TypeVar
Generic_function = TypeVar("Generic_function", bound=Callable[..., Any])
def info(func: Generic_function) -> Generic_function:
    @wraps(func)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        print('Function name: ' + func.__name__)
        print('Function docstring: ' + str(func.__doc__))
        result = func(*args, **kwargs)
        return result
    return wrapper
@info
def doubler(number: int) -> int:
    """Doubles the number passed to it"""
    return number * 2
print(doubler(4))

Если запустить mypy --strict info_decorator.py, вы получите следующий вывод:

info_decorator.py:14: error: Incompatible return value type (got "_Wrapped[[VarArg(Any), KwArg(Any)], Any, [VarArg(Any), KwArg(Any)], Any]", expected "Generic_function") [return-value]
Found 1 error in 1 file (checked 1 source file)

Это запутанная ошибка! Можете попробовать поискать ответ самостоятельно.

Что вы найдёте при поиске ответов, скорее всего, варьируется от полного игнорирования аннотаций до применения ParamSpec.

Давайте попробуем этот подход следующим!

Использование ParamSpec для аннотации типов

ParamSpec — это объект из модуля typing, созданный для указания параметров обобщённых функций, классов и типов.

class ParamSpec(object):
    """ Parameter specification variable.
    The preferred way to construct a parameter specification is via the dedicated syntax for generic functions, classes, and type aliases, where the use of '**' creates a parameter specification::
        type IntFunc[**P] = Callable[P, int]
    For compatibility with Python 3.11 and earlier, ParamSpec objects can also be created as follows::
        P = ParamSpec('P')
    Parameter specification variables exist primarily for the benefit of static type checkers. They are used to forward the parameter types of one callable to another callable, a pattern commonly found in higher-order functions and decorators. They are only valid when used in ``Concatenate``, or as the first argument to ``Callable``, or as parameters for user-defined Generics. See class Generic for more information on generic types.
    An example for annotating a decorator::
        def add_logging[**P, T](f: Callable[P, T]) -> Callable[P, T]:
            '''A type-safe decorator to add logging to a function.'''
            def inner(*args: P.args, **kwargs: P.kwargs) -> T:
                logging.info(f'{f.__name__} was called')
                return f(*args, **kwargs)
            return inner
        @add_logging
        def add_two(x: float, y: float) -> float:
            '''Add two numbers together.'''
            return x + y
    Parameter specification variables can be introspected. e.g.::
        >>> P = ParamSpec("P")
        >>> P.__name__
        'P'
    Note that only parameter specification variables defined in the global scope can be pickled.
    """

Если коротко, ParamSpec используется для построения спецификации параметров для обобщённой функции, класса или псевдонима типа.

Как работает 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('Докстрока функции: ' + str(func.__doc__))
        return func(*args, **kwargs)
    return wrapper

Здесь создаётся ParamSpec и TypeVar (переменная типа). Вы сообщаете декоратору, что он принимает Callable с обобщённым набором параметров (P), а TypeVar (R) используется для указания обобщённого типа возвращаемого значения.

Ключевой момент: P.args и P.kwargs внутри wrapper — это не просто синтаксический сахар, а точная передача сигнатуры исходной функции. Именно это позволяет статическому анализатору mypy понять, что декорированная функция сохраняет свои параметры.

Если запустить mypy на этом обновлённом коде, проверка пройдёт успешно!

Пошаговая логика применения ParamSpec

Разберём, что происходит в коде шаг за шагом:

  1. P = ParamSpec("P") — объявляем спецификацию параметров, которая будет «захватывать» сигнатуру декорируемой функции
  2. R = TypeVar("R") — объявляем переменную типа для возвращаемого значения
  3. func: Callable[P, R] — говорим, что декоратор принимает любой вызываемый объект с параметрами P и возвращаемым типом R
  4. -> Callable[P, R] — гарантируем, что декоратор возвращает функцию с той же сигнатурой
  5. args: P.args, *kwargs: P.kwargs — передаём параметры внутрь обёртки, сохраняя типовую информацию

Такой подход даёт mypy достаточно информации, чтобы корректно проверить типы как на входе, так и на выходе декоратора.

Что изменилось в PEP 695

PEP 695 обновил спецификацию параметров в Python 3.12, изменив способ добавления аннотаций типов к декораторам.

Основная идея этого PEP — «упростить» способ указания параметров типов внутри обобщённого класса, функции или псевдонима типа.

Во многих отношениях это действительно делает код чище, поскольку при использовании нового синтаксиса больше не нужно импортировать ParamSpec или TypeVar. Вместо этого всё выглядит почти как магия.

Вот обновлённый код:

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('Function name: ' + func.__name__)
        print('Function docstring: ' + str(func.__doc__))
        return func(*args, **kwargs)
    return wrapper

Обратите внимание на квадратные скобки в начале определения функции: def info[**P, R]. По сути, это неявное объявление вашего ParamSpec. R снова является типом возвращаемого значения. Остальной код такой же, как и прежде.

Когда вы запустите mypy против этой версии декоратора с аннотациями типов, вы увидите, что проверка проходит без проблем.

Что нужно учитывать при переходе на синтаксис PEP 695

Новый синтаксис доступен только начиная с Python 3.12. Если ваш проект поддерживает более ранние версии, придётся использовать явное объявление через ParamSpec и TypeVar. Это важный trade-off: чистота кода против совместимости.

Если вы работаете с Python 3.12 и выше, новый синтаксис предпочтительнее — он короче, читается естественнее и не требует дополнительных импортов из typing.

Типичные ошибки при аннотировании декораторов

На практике разработчики чаще всего допускают несколько характерных ошибок при добавлении аннотаций типов к декораторам.

Использование Callable[..., Any] вместо ParamSpec. Такой подход технически работает, но теряет информацию о конкретных параметрах функции. mypy не сможет проверить, что вы передаёте правильные аргументы в декорированную функцию.

Забытый @wraps(func). Без этого декоратора из functools обёртка потеряет __name__, __doc__ и другие атрибуты исходной функции. Это не ошибка типизации, но существенная потеря метаданных.

Попытка аннотировать wrapper через Generic_function. Именно это приводит к ошибке, которую мы видели в первом примере. TypeVar не может корректно описать трансформацию сигнатуры, которую выполняет декоратор.

Смешение старого и нового синтаксиса. Если вы используете синтаксис PEP 695 (def foo[**P, R]), не нужно дополнительно импортировать ParamSpec и TypeVar — это приведёт к конфликту.

Как проверить корректность аннотаций

Для проверки аннотаций типов в декораторах рекомендуется использовать mypy в строгом режиме. Команда выглядит так:

mypy --strict your_decorator_file.py

Флаг --strict включает все доступные проверки, включая --disallow-untyped-defs, --disallow-any-generics и ряд других. Именно в строгом режиме проявляются проблемы, которые в обычном режиме mypy молча игнорирует.

Если проверка проходит без ошибок — аннотации расставлены корректно и статический анализатор понимает сигнатуру вашего декоратора. Это особенно важно в командных проектах, где декораторы используются в нескольких модулях одновременно

Я рекомендую добавлять запуск mypy в CI-пайплайн, чтобы ошибки типизации не накапливались незаметно.

Аннотации типов по-прежнему могут быть непростой темой, однако чем новее версия Python, которую вы используете, тем лучше возможности аннотирования типов.

Конечно, поскольку сам Python не принуждает к использованию аннотаций типов, вы можете просто пропустить всё это. Но если ваш работодатель любит аннотации типов, надеемся, что эта статья вам поможет.

Ответы на эти вопросы могут быть для вас полезными

Почему TypeVar не подходит для аннотации декораторов? TypeVar не может корректно описать трансформацию сигнатуры функции внутри декоратора. При использовании TypeVar mypy выдаёт ошибку несовместимости типов возвращаемого значения, потому что обёртка (wrapper) имеет другой тип, чем исходная функция.

Начиная с какой версии Python доступен ParamSpec? ParamSpec появился в Python 3.10 как часть модуля typing. В Python 3.9 и ниже его можно получить через typing_extensions.

В чём разница между синтаксисом ParamSpec и синтаксисом PEP 695? Оба подхода решают одну задачу, но синтаксис PEP 695 (def foo[**P, R]) доступен только в Python 3.12 и выше. Он не требует явного импорта ParamSpec и TypeVar, что делает код короче. Для проектов с поддержкой Python 3.10–3.11 нужно использовать явное объявление через P = ParamSpec("P").

Обязательно ли использовать @wraps(func) вместе с аннотациями типов? @wraps(func) не связан напрямую с аннотациями типов, но его использование считается хорошей практикой. Без него декорированная функция теряет атрибуты __name__, __doc__ и __wrapped__, что затрудняет отладку и интроспекцию.

Как запустить проверку типов для декоратора с ParamSpec? Используйте команду mypy --strict your_file.py. Флаг --strict включает все строгие проверки. Если ошибок нет — аннотации расставлены корректно и mypy понимает сигнатуру декоратора.

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x