Декораторы в Python: зачем они нужны и как их создавать

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

Декораторы в Python

Для придания новой функциональности уже существующему коду в Python есть очень интересный инструмент под названием декоратор (decorator).

Это также можно назвать метапрограммированием, так как отдельные части программы меняют ее другие части в процессе компиляции.

Необходимые условия для понимания данного материала

Чтобы понять что такое декораторы, вы должны знать несколько базовых вещей в Python.

В первую очередь вас не должно удивлять, что в Python все (и даже классы!) является объектами. Задаваемые нами имена — это просто идентификаторы, привязанные к конкретным объектам. Функции также не являются исключением, это такие же объекты со своими атрибутами. К одной и той же функции может быть привязано несколько совершенно разных имен.

Вот пример:

def first(msg):
    print(msg)


first("Hello")

second = first
second("Hello")

#  Результат:
Hello
Hello

При работе данного кода обе функции, first и second, дают один и тот же результат. Это оттого, что имена first и second относятся к одному объекту.

Теперь сгустим немного краски.

Функции могут быть переданы в качестве аргументов в другие функции!

Если вы в Python пользовались функциями типа mapfilter или reduce, то уже об этом знаете.

Такие функции в Python называются функциями высшего порядка. Вот пример такой функции:

def inc(x):
    return x + 1


def dec(x):
    return x - 1


def operate(func, x):
    result = func(x)
    return result

Мы вызываем такую функцию следующим образом:

>>> operate(inc,3)
4
>>> operate(dec,3)
2

Более того, функция может возвращать другую функцию.

def is_called():
    def is_returned():
        print("Hello")
    return is_returned


new = is_called()

# Результат "Hello"
new()

Результат:

Hello

Здесь is_returned() — это вложенная функция, так как она определена внутри функции is_called(). И она возвращается каждый раз, как только мы вызываем is_called().

И еще, мы должны знать, как работают замыкания в Python.

Возвращаемся обратно к декораторам

Функции и методы в Python являются вызываемыми объектами, поскольку могут быть вызваны.

Фактически, любой объект в Python, для которого реализован специальный метод __call__(), является вызываемым. Таким образом, в наиболее общем смысле декоратор является вызываемым объектом и также возвращает вызываемый объект.

В общем и целом, декоратор принимает функцию, добавляет в нее некоторую функциональность и возвращает её.

def make_pretty(func):
    def inner():
        print("I got decorated")
        func()
    return inner


def ordinary():
    print("I am ordinary")

Когда вы запускаете данный код в интерпретаторе, происходит следующее:

>>> ordinary()
I am ordinary

>>> # Давайте декорируем функцию ordinary()
>>> pretty = make_pretty(ordinary)
>>> pretty()
I got decorated
I am ordinary

В данном примере функция make_pretty() является декоратором. Декорирование происходит вот на этом шаге:

pretty = make_pretty(ordinary)

Функция ordinary() теперь задекорирована и возвращаемая функция носит имя pretty().

Мы видим, что декоратор добавил некоторую новую функциональность в первоначальную функцию. Это напоминает упаковку для подарка. Декоратор играет роль такой упаковки. Суть самого объекта в результате декорирования не меняется. Но теперь этот объект выглядит симпатичнее (что не удивительно, он ведь был декорирован).

Обычно мы декорируем и переопределяем функцию следующим образом:

pretty = make_pretty(ordinary)

Это довольно частая конструкция, и поэтому в Python есть синтаксис для ее упрощения.

Мы используем символ @ перед названием функции и помещаем его прямо над объявлением функции, которая должна быть задекорирована. Например:

@make_pretty
def ordinary():
    print("I am ordinary")

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

def ordinary():
    print("I am ordinary")
ordinary = make_pretty(ordinary)

Такой вот синтаксический сахар для реализации декораторов.

Декорирование функций с параметрами

Разобранный нами декоратор был очень простым и мог работать только с функциями без параметров. А как быть, если у функции есть параметры? Например, вот такие:

def divide(a, b):
    return a/b

У этой функции есть два параметра, a и b и мы также знаем, что если b будет равно 0, возникнет ошибка.

>>> divide(2, 5)
0.4
>>> divide(2, 0)
Traceback (most recent call last):
...
ZeroDivisionError: division by zero

Теперь давайте сделаем декоратор, который будет проверять наличие нуля и предотвращать ошибку.

def smart_divide(func):
    def inner(a, b):
        print("Я собираюсь разделить", a, "на", b)
        if b == 0:
            print("Упс! Не могу выполнить деление")
            return

        return func(a, b)
    return inner


@smart_divide
def divide(a, b):
    print(a/b)

В новой реализации функция вернет None, если возникнет ситуация с делением на 0:

>>> divide(2,5)
Я собираюсь разделить 2 на 5
0.4

>>> divide(2,0)
Я собираюсь разделить 2 на 0
Упс! Не могу выполнить деление

Таким образом мы можем декорировать функции, имеющие параметры.

Внимательный читатель может заметить, что вложенная внутри декоратора функция inner() имеет те же параметры, что и функция, которую мы декорируем. Приняв это во внимание, мы теперь можем построить декоратор для функции с произвольным количеством параметров.

В Python эта магия выглядит следующим образом: function(*args, **kwargs). Здесь *args — это кортеж позиционных аргументов, а **kwargs — словарь ключевых аргументов. Вот пример такого декоратора:

def works_for_all(func):
    def inner(*args, **kwargs):
        print("Я могу декорировать любую функцию")
        return func(*args, **kwargs)
    return inner

Создание цепочек декораторов

В Python может последовательно применяться сразу несколько декораторов.

Иными словами, функция может быть декорирована несколько раз разными (или одними и теми же!) декораторами. Мы просто помещаем эти декораторы поверх функции, которую хотим задекорировать.

def star(func):
    def inner(*args, **kwargs):
        print("*" * 30)
        func(*args, **kwargs)
        print("*" * 30)
    return inner


def percent(func):
    def inner(*args, **kwargs):
        print("%" * 30)
        func(*args, **kwargs)
        print("%" * 30)
    return inner


@star
@percent
def printer(msg):
    print(msg)


printer("Hello")

Результат:

******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Hello
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************

Данный синтаксис

@star
@percent
def printer(msg):
    print(msg)

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

def printer(msg):
    print(msg)
printer = star(percent(printer))

Порядок, в котором мы применяем декораторы, имеет значение. Например, изменив порядок следующим образом:

@percent
@star
def printer(msg):
    print(msg)

Мы получим вот такой результат:

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************
Hello
******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%