Перейти к содержимому
Теория · Функции как значения · урок 7 из 10

Декораторы

Представьте подарок в упаковочной бумаге. Подарок внутри тот же, но, чтобы до него добраться, сначала разворачивают бумагу. В Python так же можно «упаковать» функцию: каждый вызов сначала проходит через упаковку и только потом доходит до самой функции.

Зачем это нужно? Перед каждым вызовом нескольких функций нужно печатать строку calling. Можно дописать print в тело каждой из них, но тогда одна и та же строка повторится во многих местах. Вместо этого функцию передают другой функции, а та возвращает новую, с добавленным поведением:

Python
def announce(func):
    def wrapper(*args, **kwargs):
        print("calling")
        return func(*args, **kwargs)
    return wrapper


def add(a, b):
    return a + b


add = announce(add)
result = add(2, 3)  # calling
print(result)  # 5

Давайте разберём пример по строкам:

  1. add = announce(add) отдаёт функцию add в announce и получает назад wrapper. Теперь имя add указывает на неё.
  2. add(2, 3) вызывает wrapper, и та печатает calling.
  3. wrapper вызывает исходную add с теми же аргументами и возвращает её результат 5.
  4. print(result) печатает 5.

Обёртка

Функцию, которая принимает функцию и возвращает новую, называют декоратором. Функция, которую он возвращает, — обёртка: та самая упаковочная бумага.

Здесь wrapper — замыкание: она помнит func, исходную add. При вызове обёртка печатает calling, затем вызывает исходную функцию и возвращает её результат.

Параметры *args и **kwargs позволяют обёртке принять любые аргументы и передать их исходной функции без изменений. Поэтому announce подходит для функции с любыми параметрами. До и после вызова обёртка может делать что угодно:

  • печатать строку;
  • считать вызовы;
  • менять результат.

Строка add = announce(add) вызывает announce один раз. Тело add при этом не выполняется: announce получает функцию, а не результат её вызова. После присваивания каждый вызов add идёт через обёртку. Исходная функция доступна только самой обёртке, через func.

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

Запись @

Строка @announce над def делает то же самое короче. Она равна присваиванию greet = announce(greet) сразу после def:

Python
@announce
def greet(name):
    return f"hi {name}"


message = greet("ann")  # calling
print(message)  # hi ann

Один декоратор подходит многим функциям: строку @announce ставят над каждой из них, а print написан один раз, внутри wrapper.

Декоратор срабатывает один раз, когда Python выполняет def, а не при каждом вызове. Сама announce при этом ничего не печатает. calling появляется при каждом вызове greet, потому что его печатает обёртка.

Этот вопрос часто задают на собеседовании: «Что такое декоратор и чему равна запись @dec?» Ответ из двух частей:

  • декоратор — функция, которая принимает функцию и возвращает другую, обычно обёртку;
  • @dec над def f — краткая запись f = dec(f), и имя f дальше указывает на то, что вернул dec.

Частая ошибка

В обёртке забыли return:

Python
def announce(func):
    def wrapper(*args, **kwargs):
        print("calling")
        func(*args, **kwargs)  # ✗
    return wrapper


@announce
def add(a, b):
    return a + b


print(add(2, 3))

Программа печатает calling, а затем None вместо 5. Исходная add вернула 5 обёртке, но обёртка его не вернула, а функция без return возвращает None. Пишите return func(*args, **kwargs).

Главное

  • Декоратор принимает функцию и возвращает обёртку, которая добавляет поведение до или после вызова.
  • @dec над def f — то же, что f = dec(f) сразу после определения.
  • Обёртка с *args, **kwargs подходит функции с любыми параметрами.
  • Обёртка должна вернуть результат исходной функции, иначе вызов вернёт None.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом