Декораторы Python и сохранение набора параметров декорированной функции (__signature__)

Проблема

Декораторы подменяют исходную функцию.

И при этом изменяет ее “сигнатуру” - набор параметров. У новой функции уже будет тот набор параметров, что в возвращенной декоратором функции, а не тот, что был у исходной.

Порой, это может создать проблемы. Например, если мы далее передаем эту функцию фреймворку swagger transmute, который автоматически строит swagger описание нашего API, анализируя наши обработчики запросов. Этот фреймворк описывает в swagger параметры запросов исходя из параметров наших функций- обработчиков этих запросов.

Но если мы обернули с какой-то целью наш обработчик например в такой декоратор:

def my_decorator(original_function):
    def wrapper(*args, **kwargs):
        try:
            return original_function(*args, **kwargs)
        finally:
            some_clean_up()
    return wrapper

то фреймворк уже не увидит исходных параметров.

Он будет видеть только безликие *args, **kwargs. И не построит нам корректного swagger-описания.

Как сохранить исходный набор параметров декорированной функции

Как описано в PEP0362 в Python, начиная с 3.3, можно подменять сигнатуру функции с помощью атрибута __signature__:

import inspect


def my_decorator(original_function):
    def wrapper(*args, **kwargs):
        try:
            return original_function(*args, **kwargs)
        finally:
            some_clean_up()
    wrapper.__signature__ = inspect.signature(original_function)
    return wrapper

Как сохранить прочие атрибуты функции после декорирования

Помимо этого, скорее всего мы также захотим сохранить еще ряд атрибутов функции.

Например, __doc__, что весьма немаловажно, как для автоматического создания документации, так и для doc-tests - преставьте, что иначе вы потеряете doc-тесты исходной функции.

Многие атрибуты исходной функции можно сохранить с помощью декоратора wraps модуля functools:

import inspect
import functools


def my_decorator(original_function):
    @functools.wraps
    def wrapper(*args, **kwargs):
        try:
            return original_function(*args, **kwargs)
        finally:
            some_clean_up()
    wrapper.__signature__ = inspect.signature(original_function)
    return wrapper

Только __signature__ надо сохранять отдельно.

Это связано с тем что wraps сохраняеет существующие атрибуты функции, но атрибута __signature__ скорее всего не будет в вашей исходной функции. __signature__ не добавляется ко всем функциям автоматически, хотя и корректно используется, если уже добавлено. Поэтому этот атрибут и приходится получать с помощью inspect, а не копированием из исходной функции с помощью wraps.

Опубликовано November 16, 2018