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