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 помогает автоматически перенести ключевые атрибуты, но не все, так что будьте внимательны!