Декораторы: добавляем поведение функции, не трогая её
- Объяснить, что такое декоратор и как он строится из функции высшего порядка и замыкания
- Писать декоратор с функцией-обёрткой (wrapper) и применять его через @
- Писать обёртку, подходящую для любой функции, с помощью *args и **kwargs
- Распознавать две частые ошибки — обёртку без return и return wrapper() со скобками
Что мы изучим?
- Что такое декоратор (Decorator) и чем он полезен.
- Как построить декоратор: функция, которая получает функцию и возвращает обёртку (wrapper).
- Синтаксис
@. - Обёртку, подходящую для любой функции, с
*argsи**kwargs.
Зачем это нужно?
Мы хотим печатать сообщение при каждом вызове функции — для отладки:
def add(a, b):
print("Calling add")
return a + b
def multiply(a, b):
print("Calling multiply")
return a * bПроблемы:
- Одна и та же строка копируется в каждую функцию — снова дублирование, которое запрещает DRY.
- Логика смешивается со вспомогательным кодом, который к ней не относится.
- Чтобы убрать печать, нужно пройти по всем функциям и удалить её.
Декоратор позволяет написать дополнительное поведение один раз и надеть его на любую функцию одной строкой.
Объяснение с основ
Из чего построен декоратор
Декоратор сочетает две вещи, которые мы изучили в предыдущих уроках:
- Функцию высшего порядка — он получает функцию и возвращает функцию.
- Замыкание — функция, которую он возвращает, помнит исходную функцию.
Структура всегда одна и та же:
- Декоратор получает исходную функцию (
func). - Внутри него определяется новая функция —
wrapper(обёртка), — которая делает что-то дополнительное и вызываетfunc. - Декоратор возвращает
wrapper— без скобок.
Синтаксис @
@log_call
def add(a, b):
return a + bЭто абсолютно то же самое, что написать после определения:
add = log_call(add)То есть имя add теперь указывает на обёртку, а обёртка вызывает исходную add.
Подарок (функция) не меняется. Вы заворачиваете его в бумагу (обёртку) — и тот, кто его получит, сначала встретит упаковку, а уже потом подарок. В одну и ту же бумагу можно завернуть любой подарок, и упаковку можно снять, не повредив подарок.
Синтаксис
def my_decorator(func): # 1. receive the original function
def wrapper(*args, **kwargs): # 2. a new function that wraps it
# ... something before ...
result = func(*args, **kwargs)
# ... something after ...
return result # return the original result
return wrapper # 3. return the wrapper (no parentheses)
@my_decorator # same as: my_function = my_decorator(my_function)
def my_function():
...Первый пример кода
week-2/log_call.py:
def log_call(func):
def wrapper(a, b):
print("Calling", func.__name__)
return func(a, b)
return wrapper
@log_call
def add(a, b):
return a + b
@log_call
def multiply(a, b):
return a * b
print(add(2, 3))
print(multiply(4, 5))Вывод:
Calling add
5
Calling multiply
20Разбор кода построчно
| Строка | Объяснение |
|---|---|
def log_call(func): | Декоратор получает исходную функцию. |
def wrapper(a, b): | Обёртка — получает те же параметры, что и оригинал. |
print("Calling", func.__name__) | Дополнительное поведение. __name__ — имя функции. |
return func(a, b) | Вызвать оригинал и вернуть его результат. |
return wrapper | Декоратор возвращает обёртку — без скобок. |
@log_call | add = log_call(add). Отныне add(2, 3) запускает обёртку. |
Ещё один пример
Обёртка, подходящая для любой функции. Предыдущая обёртка работает только с функциями, которые получают ровно a, b. С *args и **kwargs она принимает любые параметры и передаёт их дальше. week-2/log_any.py:
def log_call(func):
def wrapper(*args, **kwargs):
print("Calling", func.__name__, "with", args, kwargs)
return func(*args, **kwargs)
return wrapper
@log_call
def greet(name, greeting="Hello"):
return greeting + ", " + name
@log_call
def total(*prices):
return sum(prices)
print(greet("Dana", greeting="Hi"))
print(total(10, 20, 30))Вывод:
Calling greet with ('Dana',) {'greeting': 'Hi'}
Hi, Dana
Calling total with (10, 20, 30) {}
60Полезный пример: измерение времени. week-2/timed.py:
import time
def timed(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
seconds = round(time.time() - start, 2)
print(func.__name__, "took", seconds, "seconds")
return result
return wrapper
@timed
def slow_sum(n):
time.sleep(0.5)
return sum(range(n))
print(slow_sum(1000))Вывод:
slow_sum took 0.5 seconds
499500(Точное время немного меняется от запуска к запуску.) Тот же @timed можно надеть на любую функцию программы — не меняя в ней ни одной строки.
Типичная ошибка
Две ошибки, которые случаются почти у всех, кто пишет первый декоратор:
def log_call(func):
def wrapper(*args, **kwargs):
print("Calling", func.__name__)
func(*args, **kwargs)
return wrapper()- Нет
returnвнутри обёртки. Исходная функция выполняется, но её результат теряется — и декорированная функция всегда возвращаетNone. Исправление:return func(*args, **kwargs). return wrapper()со скобками. Это вызывает обёртку уже при определении и возвращает её результат вместо функции. Исправление:return wrapper.
Правило: обёртка возвращает результат, а декоратор возвращает обёртку.
Практика
Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.
Упражнение 1: Первый декоратор
Напишите декоратор с именем log_call, который печатает Calling <name> перед каждым вызовом функции, и примените его через @ к add и к multiply.
print(add(2, 3))
print(multiply(4, 5))Ожидаемый вывод:
Calling add
5
Calling multiply
20Показать решение
def log_call(func):
def wrapper(a, b):
print("Calling", func.__name__)
return func(a, b)
return wrapper
@log_call
def add(a, b):
return a + b
@log_call
def multiply(a, b):
return a * b
print(add(2, 3))
print(multiply(4, 5))Calling add
5
Calling multiply
20@log_call над def add равносильно add = log_call(add). Отныне add указывает на обёртку.
Упражнение 2: Обёртка, подходящая для любой функции
Предыдущая обёртка работает только с функциями, которые получают ровно два параметра. Перепишите log_call так, чтобы он работал с любой функцией.
@log_call
def greet(name, greeting="Hello"):
return greeting + ", " + name
@log_call
def total(*prices):
return sum(prices)
print(greet("Dana", greeting="Hi"))
print(total(10, 20, 30))Ожидаемый вывод:
Calling greet
Hi, Dana
Calling total
60Показать решение
def log_call(func):
def wrapper(*args, **kwargs):
print("Calling", func.__name__)
return func(*args, **kwargs)
return wrapper
@log_call
def greet(name, greeting="Hello"):
return greeting + ", " + name
@log_call
def total(*prices):
return sum(prices)
print(greet("Dana", greeting="Hi"))
print(total(10, 20, 30))Calling greet
Hi, Dana
Calling total
60*args и **kwargs собирают все аргументы, а func(*args, **kwargs) передаёт их дальше как есть.
Упражнение 3: Проверка ввода
Напишите декоратор с именем positive_only для функций, которые получают одно число: если число отрицательное, он выбрасывает ValueError и вообще не вызывает функцию. Сообщение включает имя функции (func.__name__) и полученное число, как в ожидаемом выводе.
@positive_only
def square_root(n):
return n ** 0.5
print(square_root(16))
try:
square_root(-4)
except ValueError as error:
print("error:", error)Ожидаемый вывод:
4.0
error: square_root needs a positive number, got -4Показать решение
def positive_only(func):
def wrapper(n):
if n < 0:
raise ValueError(func.__name__ + " needs a positive number, got " + str(n))
return func(n)
return wrapper
@positive_only
def square_root(n):
return n ** 0.5
print(square_root(16))
try:
square_root(-4)
except ValueError as error:
print("error:", error)4.0
error: square_root needs a positive number, got -4Проверка происходит до вызова func — если ввод недопустим, исходная функция вообще не выполняется.
Упражнение 4: Подсчёт вызовов
Напишите декоратор с именем count_calls, который считает, сколько раз была вызвана функция. При каждом вызове, до выполнения функции, он печатает имя функции и номер вызова — как в ожидаемом выводе.
@count_calls
def say_hi():
print("Hi")
say_hi()
say_hi()Ожидаемый вывод:
say_hi call #1
Hi
say_hi call #2
HiПоказать решение
def count_calls(func):
count = 0
def wrapper(*args, **kwargs):
nonlocal count
count += 1
print(func.__name__, "call #" + str(count))
return func(*args, **kwargs)
return wrapper
@count_calls
def say_hi():
print("Hi")
say_hi()
say_hi()say_hi call #1
Hi
say_hi call #2
Hicount принадлежит count_calls, и обёртка его помнит (замыкание). Поскольку она меняет count, нужен nonlocal.
Упражнение 5: Две ошибки
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
result.upper()
return wrapper()Декоратор должен возвращать результат функции заглавными буквами. Найдите две ошибки и исправьте их.
@shout
def greet(name):
return "hello " + name
print(greet("dana"))Ожидаемый вывод:
HELLO DANAПоказать решение
return wrapper()со скобками — вызывает обёртку уже при определении, вместо того чтобы её вернуть. Должно бытьreturn wrapper.- Обёртка ничего не возвращает —
result.upper()создаёт новую строку, но она не возвращается. Должно бытьreturn result.upper().
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@shout
def greet(name):
return "hello " + name
print(greet("dana"))HELLO DANAПравило: обёртка возвращает результат, а декоратор возвращает обёртку.
Итоги
- Декоратор = функция, которая получает функцию, заворачивает её в обёртку с дополнительным поведением и возвращает обёртку.
- Он строится из функции высшего порядка и замыкания — двух тем предыдущих уроков.
@decoratorнадdef=func = decorator(func).- С
*argsи**kwargsобёртка подходит для любой функции. - Обёртка возвращает результат (
return func(...)), а декоратор возвращает обёртку (return wrapper, без скобок).