Использование functool.wraps в декораторах Python
Давайте предположим, что у нас есть простой декоратор, «mydeco», который берет вывод функции и помещает его в строку, за которой следуют три восклицательных знака:
def mydeco(func): def wrapper(*args, **kwargs): return f'{func(args, **kwargs)}!!!' return wrapper
Давайте теперь используем наш декоратор для двух разных функций:
@mydeco def add(a, b): '''Add two objects together, the long way''' return a + b
@mydeco def mysum(*args): '''Sum any numbers together, the long way''' total = 0 for one_item in args: total += one_item return total
Что происходит, когда я запускаю эти функции? Они работают как мы ожидали:
>>> add(10, 20) '30!!!'
>>> mysum(10, 20, 30, 40, 50) '150!!!
Фантастика! Мы возвращаем результат каждой функции в виде строки с восклицательными знаками. Декоратор работает.
Но есть несколько проблем с тем, что мы сделали. Например, что если я запрашиваю каждую функцию о ее имени:
>>> add.__name__ 'wrapper'
>>> mysum.__name__ 'wrapper'
Атрибут __name__, который возвращает нам имя функции при ее определении, теперь отражает возвращенную внутреннюю функцию «wrapper» используемую в нашем декораторе. Это так и есть, но это не то что нам нужно.
Может быть еще хуже, если мы попросим вернуть строку документации:
>>> help(add) Help on function wrapper in module __main__: wrapper(*args, **kwargs)
>>> help(mysum) Help on function wrapper in module __main__: wrapper(*args, **kwargs)
Другими словами: теперь мы получаем строку документации и функцию «wrapper», внутренней функции. И это проблема, потому что теперь мы не может получить основные атрибуты декорированной функции.
Мы можем решить эту проблему, хотя бы частично, назначив атрибуты __name__ и __doc__ в нашем декораторе:
def mydeco(func): def wrapper(*args, **kwargs): return f'{func(args, **kwargs)}!!!' wrapper.__name__ = func.__name__ wrapper.__doc__ = func.__doc__ return wrapper
Если мы используем эту версию декоратора, то каждый раз, когда мы возвращаем «wrapper» от нашего декоратора, мы вручную присваиваем имя исходной функции и строку документации для нее.
>>> help(add) Help on function add in module __main__: add(*args, **kwargs) Add two objects together, the long way
>>> help(mysum) Help on function mysum in module __main__: mysum(*args, **kwargs) Sum any numbers together, the long way
Хорошая новость заключается в том, что мы исправили проблему имен и строк документации. Но сигнатура функции все еще остается общей *args и **kwargs.
Решение заключается в использовании functools.wraps. Она предназначен для решения именно этих проблем. Ирония, конечно, заключается в том, что это может доставить больше проблем, чем обычно это делают декораторы, потому что functools.wraps — это … декоратор, который принимает аргумент! Вот как это выглядит:
from functools import wraps def mydeco(func): @wraps(func) def wrapper(*args, *kwargs): return f'{func(args, **kwargs)}!!!' return wrapper
Обратите внимание на то, что мы сделали здесь: мы использовали декоратор «functool.wraps», чтобы декорировать нашу внутреннюю функцию, функцию «wrapper». Применяя этот декоратор «wraps» к нашей внутренней функции, мы копируем имя, строку документации и сигнатуру функции в нашу внутреннюю функцию, избегая проблем, с которыми мы сталкивались ранее:
>>> help(add) Help on function add in module main: add(a, b) Add two objects together, the long way >>> help(mysum) Help on function mysum in module main: mysum(*args) Sum any numbers together, the long way
Ее использование почти ничего не стоит (т. е. одна строка кода) и она делает вашу декорированную функцию более естественной.
Оригинальная статья: Making your Python decorators even better, with functool.wraps
Ошибка в
return f'{func(args, **kwargs)}!!!’
забыли * перед args
«это может доставить больше проблем, чем обычно это делают декораторы»
было бы интересно узнать в этом месте поподробнее 🙂
плохо перевели, в оригинале имелось в виду что это еще сложнее осмыслить чем обычные декораторы