Декораторы – это очень полезные инструменты. Поэтому предлагаем пройти шаг за шагом в их освоении. Понять, что они собой представляют и как их использовать.
Создание декоратора
Итак, что же такое декоратор? Прежде чем ответить на этот вопрос, давайте посмотрим, как он выглядит.
@decorator
def func():
pass
Code language: CSS (css)
Здесь мы видим, как декоратор с именем decorator применяется к функции func().
Из этого можно сделать вывод, что декораторы – это то, что применяется к функциям.
Фактически, декораторы сами являются функциями, принимая в качестве аргумента одну функцию и возвращая другую.
Более того, приведенный выше код делает то же самое, что и следующий.
def func(): pass func = decorator(func)
Теперь определение имеет немного больше смысла. В этом коде ─ который, повторяю, работает точно так же, как и первый ─ наш декоратор является функцией. Которой мы передаем в качестве аргумента другую функцию (func()), и которая возвращает новую функцию, присвоенную func.
Синтаксис @ позволяет четко видеть, какие декораторы работают над функцией.
Перейдем к конкретному случаю. В качестве примера возьмем следующую функцию.
def add(a, b):
return a + b
Code language: JavaScript (javascript)
Теперь предположим, что мы хотим печатать сообщение на экране каждый раз, когда вызывается эта функция. Для этого мы можем использовать декоратор.
@debug
def add(a, b):
return a + b
Code language: JavaScript (javascript)
Теперь пришло время написать код для нашего декоратора. Напомним, что это должна быть функция, принимающая в качестве аргумента другую функцию.
def debug(f): pass
Она также должна была вернуть новую функцию. Поэтому сделаем так.
def debug(f):
def new_function():
pass
return new_function
Code language: JavaScript (javascript)
Напомним, что Python позволяет нам определять функции внутри других функций.
Итак, возвращаясь к нашему декоратору, как мы говорили, код будет соответствовать следующему.
def add(a, b):
return a + b
add = debug(add)
Code language: JavaScript (javascript)
Поэтому для того, чтобы все вызовы add() в нашей программе продолжали работать, декоратор должен вернуть новую функцию с той же структурой аргументов и операцией, что и оригинальная (то есть, принять два объекта в качестве аргументов и вернуть сумму, которая получается в результате).
Однако наша new_function() не имеет аргументов. Давайте это исправим.
def debug(f):
def new_function(a, b):
pass
return new_function
Code language: JavaScript (javascript)
Наконец, нам не нужно дублировать код add() в new_function(), поскольку он уже предоставлен в качестве аргумента. Мы можем просто сделать:
def debug(f):
def new_function(a, b):
return f(a, b)
return new_function
Code language: JavaScript (javascript)
Идеально! Теперь декоратор может быть применен к нашей функции без ошибок.В результате на экране должно появиться сообщение.
def debug(f):
def new_function(a, b):
print("Функция add() вызвана!")
return f(a, b)
return new_function
@debug
def add(a, b):
return a + b
print(add(7, 5))
Code language: PHP (php)
Поздравляю! Это был наш первый эксперимент с декораторами.
Давайте пойдем немного дальше. Рассмотрим другую функцию, которая возвращает противоположное число n.
def neg(n):
return n * -1
Code language: JavaScript (javascript)
Поскольку наш декоратор был создан специально для функции add(), которая требовала два аргумента, он не сможет быть применен в данном случае.
@debug
def neg(n):
return n * -1
# TypeError!
print(neg(5))
Code language: PHP (php)
Ошибка заключается в следующем.
TypeError: new_function() missing 1 required positional argument: 'b'
Code language: JavaScript (javascript)
Поэтому решением будет изменение структуры функции new_function(), чтобы она принимала один аргумент (как neg()). ─ что теперь приведет к отказу декоратора при применении в add()─ или включает более общую нотацию для приема всех видов позиционных аргументов и аргументов по имени.
def debug(f):
def new_function(*args, **kwargs):
print(f"Функция {f.__name__}() вызвана!")
return f(*args, **kwargs)
return new_function
Code language: PHP (php)
(Примечание: не путайте “f” в третьей и четвертой строках! Первый указывает на то, что мы хотим отформатировать строку; второй относится к аргументу f).
Здесь мы изменили сообщение, чтобы оно всегда указывало имя соответствующей функции (f.__name__).
Теперь наш декоратор готов к применению к любому типу функций.
Декораторы с аргументами
Интереснее всего то, что декораторы могут иметь аргументы.
Предположим, что, следуя нашему примеру, мы хотим, чтобы отладка включала опцию прерывания выполнения программы и запуска отладчика при вызове функции.
Синтаксис может выглядеть следующим образом.
@debug(breakpoint=True)
def func():
pass
Code language: CSS (css)
Как мы можем это реализовать?
Давайте переведем код в его эквивалент без @-синтаксиса.
def func():
pass
func = debug(breakpoint=True)(func)
Code language: PHP (php)
Здесь становится понятнее, что теперь аргументом функции debug() не может быть f, а скорее breakpoint. И что она должна вернуть наш предыдущий декоратор, которому будет передана функция func().
Это будет что-то похожее на:
def debug(breakpoint=False):
def debug_decorator(f):
def new_function(*args, **kwargs):
print(f"Функция {f.__name__}() вызвана!")
return f(*args, **kwargs)
return new_function
return debug_decorator
Code language: PHP (php)
Как вы видите, мы создаем новую функцию debug(), которая будет отвечать за прием аргументов, и внутри нее размещаем наш декоратор, теперь с именем debug_decorator.
Далее мы включаем код для запуска отладчика, когда это необходимо.
import pdb
def debug(breakpoint=False):
def debug_decorator(f):
def new_function(*args, **kwargs):
print(f"Функция {f.__name__}() вызвана!")
if breakpoint:
pdb.set_trace()
return f(*args, **kwargs)
return new_function
return debug_decorator
Code language: PHP (php)
Давайте проведем несколько тестов:
@debug(breakpoint=True)
def add(a, b):
return a + b
@debug() # Нужны скобки!
def neg(n):
return n * -1
print(neg(5))
print(add(7, 5)) # Этот вызов запускает отладчик.
Code language: PHP (php)
Отлично!
Дополнительные моменты
Распространенной проблемой при использовании декораторов является то, что, поскольку они “инкапсулируют” функцию, к которой применяются, мы можем увидеть, что теряем возможность доступа к атрибутам исходной функции: например, к имени и документации.
Рассмотрим следующий код.
def debug(f):
def new_function(*args, **kwargs):
print(f"Функция {f.__name__}() вызвана!")
return f(*args, **kwargs)
return new_function
@debug
def neg(n):
"Возвращаем обратное значение n."
return n * -1
print(neg.__name__) # new_function
help(neg)
Code language: PHP (php)
Мы видим, что атрибуты __name__ и __doc__ (доступ к которым осуществляется с помощью help()) возвращают значения функции new_function().
Чтобы избежать этого, мы можем использовать стандартный декоратор functools.wraps(), который позаботится о копировании всех атрибутов из исходной функции в новую.
from functools import wraps
def debug(f):
@wraps(f)
def new_function(*args, **kwargs):
print(f"Функция {f.__name__}() вызвана!")
return f(*args, **kwargs)
return new_function
Code language: JavaScript (javascript)
Все просто!