functools.wraps
Что делает functools.wraps?
functools.wraps - это удобный интерфейс для functools.update_wrapper, который автоматически переносит ключевую метаинформацию от вызываемого объекта (функции или класса) к его обёртке. Обычно обёртка - это функция, но это может быть любой вызываемый объект, например, класс.
Помимо параметра wrapped, который принимает вызываемый объект, есть ещё два аргумента:
@functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
Параметр assigned указывает, какие атрибуты следует перенести от оригинального объекта к обёртке. Значение по умолчанию WRAPPER_ASSIGNMENTS включает следующие атрибуты:
- __module__: Имя модуля, в котором объявлен объект
- __name__: Имя объекта
- __qualname__: Подробное имя объекта
- __doc__: Строка документации объекта
Параметр updated имеет значение по умолчанию ('__dict__',), указывающее, какие атрибуты обёртки должны быть обновлены значениями от оригинального объекта. По умолчанию, атрибут __dict__ обёртки обновляется парами ключ-значение из __dict__ оригинального объекта.
Также декоратор wraps добавляет новый атрибут __wrapped__, который хранит ссылку на обёрнутую функцию или класс. Это полезно для получения метаинформации об оригинальном объекте, которую functools.wraps не переносит автоматически, например, __defaults__.
Декораторы без functools.wraps
Рассмотрим функцию с декоратором без переноса метаинформации. Пример декоратора:
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__ = {}
Метаданные не были перенесены на обёртку.
Декораторы с 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 0x7b49d5fccc10>}
Метаинформация была успешно перенесена.
Перенос дополнительной метаинформации
Можно передавать свои аргументы для переноса дополнительной метаинформации:
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
Теперь метаинформация о значениях по умолчанию тоже будет перенесена:
print(f'{hello_world.__defaults__ = }')
Вывод:
hello_world.__defaults__ = ('earth',)
Заключение
Перенос метаинформации при использовании декораторов важен для упрощения отладки и интеграции с другими частями языка. functools.wraps помогает автоматически перенести ключевые атрибуты, но не все, так что будьте внимательны!