Декоратор Python скрыл сигнатуру: что восстанавливает functools.wraps

После добавления декоратора функция расчёта по-прежнему возвращает правильную сумму. Однако генератор справки показывает wrapper, документация исчезает, а система подключения обработчиков больше не видит обязательный параметр order_id. Ошибка находится на границе двух контрактов: как функция исполняется и что другие инструменты могут о ней узнать.

Что видят инструменты

Обычная вложенная функция является самостоятельным объектом. У неё собственные __name__, __doc__ и сигнатура. Передача *args и **kwargs позволяет переслать вызов, но никак не сообщает интроспекции, как устроена исходная функция. Это особенно заметно в библиотеках, которые строят интерфейс по параметрам callback: они могут принять обёртку за функцию с произвольными аргументами.

functools.wraps переносит выбранные метаданные и устанавливает __wrapped__. По этой ссылке inspect.signature обычно доходит до исходной функции. Здесь важно направление: wraps помогает инструменту найти объявление, однако сама wrapper продолжает принимать произвольные аргументы. Если она выполнит побочный эффект перед вызовом оригинала, неправильные аргументы обнаружатся уже после этого эффекта.

В примере декоратор считает только корректно сформированные вызовы. Поэтому перед изменением счётчика мы явно связываем аргументы с сохранённой сигнатурой через bind. Для функции с именованным параметром tax это позволяет отвергнуть ошибку до увеличения счётчика. Такой порядок полезен, когда обёртка списывает квоту, пишет аудит или начинает транзакцию: сначала проверяется форма вызова, затем начинается работа.

Сохранить контракт вызова

from functools import wraps
from inspect import signature

accepted = []

def counted(func):
    contract = signature(func)

    @wraps(func)
    def wrapper(*args, **kwargs):
        contract.bind(*args, **kwargs)
        accepted.append(func.__name__)
        return func(*args, **kwargs)

    return wrapper

@counted
def total(order_id: int, *, tax: int = 20) -> int:
    """Calculate a demo total."""
    return order_id + tax

assert total.__name__ == "total"
assert total.__doc__ == "Calculate a demo total."
assert str(signature(total)) == "(order_id: int, *, tax: int = 20) -> int"
assert total(7, tax=3) == 10
try:
    total(7, 3)
except TypeError:
    pass
else:
    raise AssertionError("keyword-only argument accepted")
assert accepted == ["total"]
print(total.__name__, signature(total), len(accepted))

Сохраните блок в файл и запустите Python. Вывод показывает исходное имя, параметры order_id и tax и один принятый вызов. Проверка ошибки намеренно передаёт tax позиционно. Внешняя обёртка могла бы принять такую пару, но bind восстанавливает правило keyword-only до записи в accepted. Аннотация int в сигнатуре при этом остаётся метаданными: bind проверяет расположение аргументов, а не тип переданного значения.

Проверить цепочку обёрток

В простом декораторе без подготовительных действий отдельный bind часто избыточен: исходная функция сама проверит вызов. Он нужен там, где момент отказа меняет наблюдаемое состояние. Подумайте также, разрешает ли ваш декоратор дополнительные параметры только для себя. Тогда интерфейс намеренно отличается от оригинала, и простое копирование исходной сигнатуры может ввести инструмент в заблуждение.

Цепочку из нескольких декораторов проверяйте целиком. Достаточно одного слоя без __wrapped__, чтобы обход остановился раньше. Для диагностики сравните signature(callback) и signature(callback, follow_wrapped=False): первое показывает объявленный контракт, второе — фактическую сигнатуру внешней функции. Это объясняет ситуацию, когда справка выглядит правильно, а трассировка всё ещё проходит через wrapper.

Регрессионный тест должен сохранить и результат, и форму интерфейса: имя, документацию, keyword-only параметр и отсутствие побочного эффекта при неправильном вызове. Если тестировать только сумму, дефект интеграции со справкой или диспетчером обработчиков останется незамеченным. Оборачивая метод, дополнительно проверьте вызов через экземпляр: привязка self должна оставаться обычной для функции, помещённой в класс.

Продолжить практику

Декораторы становятся понятнее, когда видно, какие объекты и ссылки они добавляют. В курсе «Python для профессионалов» есть декораторы, functools и специальные возможности объектов. Откройте программу и продолжите от сохранения интерфейса к собственным обёрткам с параметрами и понятным порядком выполнения.

Продолжить на практике

  • Python для профессионалов ↗

    Python для профессионалов — курс для людей, стремящихся углубить свои знания языка. Вы изучите итераторы, генераторы, декораторы, современные возможности Python (3.8+), освоите метапрограммирование, асинхронность, внутреннее устройство интерпретатора, инструменты оптимизации и профилирования, а также получите уверенность в использовании продвинутых возможностей стандартной библиотеки.