code
Декораторы в Python — это мощный инструмент, который позволяет модифицировать поведение функций или классов без изменения их кода. Один из наиболее распространенных примеров использования декораторов — хранение первоклассных функций. Это делает, например, декоратор приложения Flask.route
.
Однако из-за базовой механики языка обертывание одного объекта над другим может привести к потере ценных метаданных от инкапсулированного объекта. Это может привести к проблемам, например, при использовании декораторов для логирования или кэширования.
Вот почему при разработке собственных декораторов Python важно использовать декоратор из модуля functiontools библиотеки Python Standard Library. Этот декоратор, называемый 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__
.
Во-первых, давайте посмотрим, как выглядит функция с декоратором, когда нет передачи метаданных. Ниже приведен декоратор, который будем использовать в примере. Этот декоратор на самом деле ничего не делает, но обратите внимание, что у него есть свой собственный 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, вам просто нужно добавить его в объект обертки следующим образом:
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
не перемещает каждый атрибут автоматически, и у вас не должно быть никаких проблем.