Декораторы с параметрами. Сохранение свойств декорируемых функций

Курс по Python: https://stepik.org/course/100707

На предыдущем занятии, мы познакомились с декораторами и рассмотрели некоторые примеры их работы. Здесь немного углубимся в эту тему и начнем с декораторов, которым можно дополнительно передавать аргументы.

Предположим, что мы создаем декоратор для вычисления производной функции. Используя уже имеющиеся знания, это можно сделать, следующим образом:

def func_decorator(func):
    def wrapper(x, *args, **kwargs):
        dx = 0.0001
        res = (func(x + dx, *args, **kwargs) - func(x, *args, **kwargs)) / dx
        return res
 
    return wrapper

И к функции, например, sin(x) применить этот декоратор (не забываем import math):

@func_decorator
def sin_df(x):
    return math.sin(x)
 
 
df = sin_df(math.pi/3)
print(df)

После запуска программы видим искомый результат. Но, что если, мы хотим управлять значением dx, передавая его как аргумент декоратору? Прописывать еще один параметр непосредственно у func_decorator, не лучший вариант:

def func_decorator(func, dx=0.0001):

В этом случае, у нас перестанет работать синтаксис с собачкой:

@func_decorator(dx=0.01)
def sin_df(x):
    return math.sin(x)

и попытка выполнить такую программу, приведет к ошибке. Для корректной реализации такой задачи следует обернуть имеющийся у нас декоратор в еще одну внешнюю функцию:

def df_decorator(dx=0.0001):
    def func_decorator(func):
        def wrapper(x, *args, **kwargs):
            res = (func(x+dx, *args, **kwargs) - func(x, *args, **kwargs)) / dx
            return res
 
        return wrapper
   return func_decorator

И применить уже ее к функции синуса:

@df_decorator(dx=0.01)
def sin_df(x):
    return math.sin(x)

В этом случае, аргумент будет передан в первую функцию, ссылка sin_df – во вторую вложенную функцию, а затем, через ссылку на третью функцию, мы можем вычислять значение производной:

df = sin_df(math.pi/3)

Все это эквивалентно следующим вызовам:

f = df_decorator(dx=0.01)
sin_df = f(sin_df)

Или, в более краткой форме:

sin_df = df_decorator(dx=0.01)(sin_df)

Но, запись через символ «собачки» куда нагляднее и проще, поэтому он, как правило, используется на практике.

Контроль имени и описания декорируемой функции

Во второй части занятия я хочу коснуться проблемы потери имени и описания декорируемой функции. О чем здесь речь? Вот смотрите, имя нашей декорированной функции можно узнать по специальному свойству __name__:

print(sin_df.__name__)

Сейчас оно принимает значение wrapper, что не удивительно, так как это и есть ссылка на эту функцию. Но изначально имя было другим (декоратор в комментарий и повторяем) – sin_df. Иногда важно сохранять исходные имена и при декорировании. Как это сделать? В самом простом варианте мы можем вручную внутри функции func_decorator изменить имя, оставив исходное:

def df_decorator(dx=0.0001):
    def func_decorator(func):
        def wrapper(x, *args, **kwargs):
            res = (func(x+dx, *args, **kwargs) - func(x, *args, **kwargs)) / dx
            return res
 
        wrapper.__name__ = func.__name__
        return wrapper
 
    return func_decorator

И, как видим, это работает. И тот же самый фокус можем проделать с описанием функции:

@df_decorator(dx=0.01)
def sin_df(x):
    """Функция для вычисления производной синуса"""
    return math.sin(x)

Это описание доступно с помощью специального свойства __doc__:

print(sin_df.__doc__)

Для декорированной функции оно равно None, а для исходной – заданная строка. Также сохраняем описание, прописав в функции func_decorator строчку:

wrapper.__doc__ = func.__doc__

Все, теперь у нас сохраняются и имена и описания декорируемых функций.

Но есть более простой и продвинутый способ сохранять свойства __name__ и __doc__ - с помощью специального декоратора:

from functools import wraps

Если мы декорируем вложенную функцию wrapper:

@wraps(func)

то строчки можно убрать (я поставлю в комментарии):

# wrapper.__name__ = func.__name__
# wrapper.__doc__ = func.__doc__

И, выполняя программу, видим, что имя функции и ее описание остаются прежними. То есть, декоратор wraps автоматически сохраняет эти свойства.

На этом мы завершим тему декораторов и функций языка Python. Этого материала, вам вполне хватит для программирования большинства задач и, кроме того, позволит понимать уже существующие программы. До встречи на следующем уроке!

Курс по Python: https://stepik.org/course/100707

Видео по теме