Глубокое погружение в декоратор Python functools.wraps

image

Декораторы в Python — это мощный инструмент, который позволяет модифицировать поведение функций или классов без изменения их кода. Один из наиболее распространенных примеров использования декораторов — хранение первоклассных функций. Это делает, например, декоратор приложения Flask.route.

Однако из-за базовой механики языка обертывание одного объекта над другим может привести к потере ценных метаданных от инкапсулированного объекта. Это может привести к проблемам, например, при использовании декораторов для логирования или кэширования.

Вот почему при разработке собственных декораторов Python важно использовать декоратор из модуля functiontools библиотеки Python Standard Library. Этот декоратор, называемый wraps(), сохраняет метаданные исходной функции, что позволяет избежать возможных проблем.

Что делает functools.wraps?

Параметр assigned сообщает functools.wraps, какие атрибуты должны быть взяты из завернутого объекта и перенесены в обертку. Параметр имеет кортей по умолчанию WRAPPER_ASSIGNMENTS, который имеет следующие атрибуты dunder (В Python термин "dunder attributes" (от английского "double underscore attributes") используется для обозначения атрибутов, имена которых начинаются и заканчиваются двумя символами подчеркивания):

  • __Module__: Имя модуля, в котором объявлен объект.
  • __Name__: Имя объекта.
  • __Qualname__: Более подробная версия __name__.
  • __Doc__: Строка doc в верхней части объекта.

Параметр updated также имеет значение кортежа по умолчанию (__dict__,). Параметр updated сообщает декоратору wraps, какие атрибуты вызываемой обертки должны быть обновлены значениями исходного объекта. По умолчанию атрибут __dict__ объекта обертывания обновляется парами ключ-значение __dict__ из обернутого объекта.

Декоратор обертывания также добавляет новый атрибут к объекту обертывания под названием __wrapped__, который содержит указатель на прилагаемую функцию или класс. Это очень полезно, так как позволяет заглянуть в фактический объект, который заворачивается, чтобы увидеть больше метаданных об объекте, которые functools.wraps не включает автоматически, например, __defaults__.

Декораторы без functools.wraps

Во-первых, давайте посмотрим, как выглядит функция с декоратором, когда нет передачи метаданных. Ниже приведен декоратор, который будем использовать в примере. Этот декоратор на самом деле ничего не делает, но обратите внимание, что у него есть свой собственный docstring (функция обертки) и __name__ функции обертывания называемый wrapper.

def example_decorator(func):
def wrapper(*args, **kwargs):
    """Wrapper function"""
    return func(*args, **kwargs)
	
return wrapper

Теперь давайте воспользуемся декоратором для следующей функции и посмотрим на некоторые из ее метаданных:

@example_decorator
def hello_world(planet: str = 'earth'):
    """Say hello to a world"""
    print(f"Hello, {planet}!")

# Проверка метаданных завернутой функции
print(f'{hello_world.__name__        =  }')
print(f'{hello_world.__doc__         =  }')
print(f'{hello_world.__annotations__ =  }')
print(f'{hello_world.__dict__        =  }')

Вывод:

hello_world.__name__        =  'wrapper'
hello_world.__doc__         =  'Wrapper function'
hello_world.__annotations__ =  {}
hello_world.__dict__        =  {}

Из выходных данных мы видим, что ни что из метаданных не было перенесено в функцию обертывания.

Кроме того, если мы печатаем объект функции:

print(hello_world)

Мы получаем следующий вывод, который не дает нам никакой информации о функции hello_world.

<function example_decorator.<locals>.wrapper at 0x102d50f40>

Декораторы с functools.wraps

Теперь давайте сделаем тот же декоратор, но добавим декоратор functools.wraps. Чтобы использовать ``functools.wraps, вам просто нужно добавить его в объект обертки следующим образом:

from functools import wraps

def example_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """Wrapper function"""
        return func(*args, **kwargs)

    return wrapper

И если мы теперь посмотрим на те же метаданные, что и раньше:

@example_decorator
def hello_world(planet: str='earth'):
    """Say hello to a world"""
    print(f"Hello, {planet}!")

# Проверка метаданных завернутой функции
print(f'{hello_world.__name__         =  }')
print(f'{hello_world.__doc__          =  }')
print(f'{hello_world.__annotations__  =  }')
print(f'{hello_world.__dict__         =  }')

Мы получаем следующий результат:

hello_world.__name__         =  'hello_world'
hello_world.__doc__          =  'Say hello to a world'
hello_world.__annotations__  =  {'planet': <class 'str'>}
hello_world.__dict__         =  {'__wrapped__': <function hello_world at 0x1045f8f40>}

Мы видим, что атрибут __name__ был обновлен в соответствии с именем функции, которую обертывает декоратор. Также обновляется строка ``docstrigзавернутой функции, а также аннотации статической типизации. Атрибутdictтакже содержит все атрибутыhello_world(что ничего не значит, так как это функция), а также имеет атрибутwrapped`, который содержит прямую ссылку на исходную функцию.

Как упоминалось выше, атрибут __wrapped__ содержит указатель на исходный объект. Это может быть очень полезно, если другая программа или разработчик захочет интроспектировать/получить больше информации об инкапсулированном объекте. Для этого существует еще один модуль стандартной библиотеки Python под названием inspect. В этом модуле есть функция под названием inspect.signature(), которая на самом деле ищет атрибут __wrapped__ в __dict__, и если он найден, он следует за значением __wrapped__, чтобы иметь возможность проверить фактический объект.

Тем не менее, вы также можете вручную использовать атрибут __wrapped__, чтобы получить дополнительную информацию об инкапсулированном объекте. Например, functools.wraps не передает автоматически значения по умолчанию в объект обертки (поскольку классы не имеют атрибута __defaults__, а класс может быть оберткой), но с помощью __wrapped__ мы можем увидеть значения по умолчанию, которые хранятся в атрибуте __defaults__:

print(f'{hello_world.__dict__["__wrapped__"].__defaults__ =  }')

И мы видим, что вывод такой, как ожидалось. Аргумент planet из функции выше имеет значение по умолчанию earth.

Наконец, мы также можем распечатать объект функции. Он показывает правильную функцию:

print(hello_world)

Вывод:

<function hello_world at 0x100b28720>

Передача дополнительных метаданных

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

Теперь, если мы попытаемся распечатать атрибут __defaults__, мы увидим (earth), потому что параметр planet в функции hello_world имеет значение по умолчанию earth:

MORE_WRAPPER_ASSIGNMENTS = (
    '__module__', '__name__',
    '__qualname__', '__annotations__',
    '__doc__', '__defaults__',
    '__kwdefaults__'
)
    
def example_decorator(func):
    @wraps(func, assigned=MORE_WRAPPER_ASSIGNMENTS)
    def wrapper(*args, **kwargs):
        """Wrapper function"""
        return func(*args, **kwargs)

    return wrapper

@example_decorator
def hello_world(planet: str='earth'):
    """Say hello to a world"""
    print(f"Hello, {planet}!")

Теперь, если мы попытаемся распечатать атрибут __defaults__, мы увидим (earth), потому что параметр planet в функции hello_world имеет значение по умолчанию earth:

print(f'{hello_world.__defaults__ =  }')

И, вывод:

hello_world.__defaults__ =  ('earth',)

Заключение

Сохранение метаданных объектов, которые используют декораторы, чрезвычайно важно, так как это приводит к коду, который легче отлаживать и гарантирует, что ваши объекты по-прежнему работают с другими частями языка, такими как способность программы получать доступ к внутренней информации о себе. Добавление functools.wraps в ваши декораторы позволяет легко переносить наиболее важные атрибуты в объект обертки. Тем не менее, просто помните, что functools.wraps не перемещает каждый атрибут автоматически, и у вас не должно быть никаких проблем.

Источник