Функция update_wrapper() и декоратор wraps

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Смотреть материал на YouTube | RuTube

Мы с вами уже знаем, что любая функция в Python имеет два важных атрибута:

  • __name__ - название (имя) функции;
  • __doc__ - описание функции (если оно присутствует).

Например, если объявить функцию tr_per следующим образом:

def tr_per(a, b, c, add=0, mul=1):
    "Вычисление периметра фигуры со сторонами a, b, c"
    print(a, b, c, add, mul)
    return (a + b + c) * mul + add

С выводом атрибутов __name__ и __doc__ в консоль:

print(tr_per.__name__)
print(tr_per.__doc__)

то увидим строчки:

tr_per
Вычисление периметра фигуры со сторонами a, b, c

Однако, после декорирования этой функции, например, с помощью partial:

rect_per = functools.partial(tr_per, 0, mul=2)

Эти атрибуты меняют свои значения:

print(rect_per.__doc__)
print(rect_per.__name__) # AttributeError

При этом часто требуется, чтобы значения этих атрибутов соответствовали изначальным, как у декорируемой функции (в нашем примере, tr_per). Для этого можно воспользоваться функцией update_wrapper() модуля functools, которая копирует информацию атрибутов __name__ и __doc__ из одной функции в другую. В нашем случае ее следует вызвать следующим образом:

functools.update_wrapper(rect_per, tr_per)

Теперь атрибуты rect_per.__doc__ и rect_per.__name__ будут такими же, как и у функции tr_per.

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

На одном из занятий по базе языка Python мы с вами рассматривали создание своих собственных декораторов функций. В них мы также сохраняли метаданные __name__ и __doc__, но делали это с помощью декоратора functools.wraps. Для целостности материала, давайте еще раз посмотрим, как это делается.

Сформируем декоратор avr_decor для функции tr_per(), который позволит вычислять среднее арифметическое трех переданных чисел a, b, c:

def avr_decor(func):
    def wrapper(*args, **kwargs):
        result = func(*args, add=0, mul=1/3, **kwargs)
        return result
 
    return wrapper

И сразу применим его к функции tr_per():

@avr_decor
def tr_per(a, b, c, add=0, mul=1):
    "Вычисление периметра фигуры со сторонами a, b, c"
    print(a, b, c, add, mul)
    return (a + b + c) * mul + add

Для примера вычислим среднее арифметическое и выведем результат с метаданными декорированной функции:

res = tr_per(1, 2, 3)
 
print(res)  # 2.0
print(tr_per.__name__)  # wrapper
print(tr_per.__doc__)  # None

Как видите, теперь функция вычисляет среднее арифметическое, но атрибуты __name__ и __doc__ не сохранились. Поправить это можно несколькими способами. Либо воспользоваться уже знакомой нам функцией update_wrapper() следующим образом:

def avr_decor(func):
    def wrapper(*args, **kwargs):
        result = func(*args, add=0, mul=1/3, **kwargs)
        return result
 
    functools.update_wrapper(wrapper, func)
    return wrapper

Либо применить декоратор wraps к вложенной функции wrapper:

def avr_decor(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, add=0, mul=1/3, **kwargs)
        return result
 
    return wrapper

Результат будет тем же самым.

Декоратор с помощью класса

Аналогичный декоратор можно реализовать и с помощью класса. Например:

class AvrDecor:
    def __init__(self, func):
        self.func = func
        functools.update_wrapper(self, func)
 
    def __call__(self, *args, **kwargs):
        return self.func(*args, add=0, mul=1/3, **kwargs)

Когда применяется декоратор AvrDecor:

@AvrDecor
def tr_per(a, b, c, add=0, mul=1):
    "Вычисление периметра фигуры со сторонами a, b, c"
    print(a, b, c, add, mul)
    return (a + b + c) * mul + add

то автоматически создается объект этого класса и вызывается инициализатор с передачей в него ссылки на декорируемую функцию tr_per. Ссылка на функцию сохраняется в локальном атрибуте self.func, а в самом объекте self создаются атрибуты __name__ и __doc__ с помощью функции update_wrapper().

После декорирования переменная tr_per будет ссылаться уже на объект класса AvrDecor, а не на объект-функцию. При этом операция вызова:

res = tr_per(1, 2, 3)

передаст управление магическому методу __call__() класса AvrDecor. Этот метод вызовет функцию self.func, указанную при декорировании, то есть, исходную функцию tr_per(), и возвратит результат ее работы.

При выводе результатов декорированной функции и ее мета-данных:

print(res)  # 2.0
print(tr_per.__name__)  # tr_per
print(tr_per.__doc__)  # Вычисление периметра фигуры со сторонами a, b, c

увидим ожидаемый результат.

Таким образом, оба инструмента - update_wrapper и @wraps – помогают сохранить оригинальные метаданные функции при её декорировании. Поэтому если важно поддерживать исходную информацию о функциях рекомендуется использовать именно эти методы, особенно при написании сложных декораторов, где важна читаемость кода и совместимость с инструментами разработки интегрированной среды и генераторов документации.

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Видео по теме