Использование functool.wraps в декораторах Python

Spread the love

Давайте предположим, что у нас есть простой декоратор, «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


Spread the love

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *