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

В Python декоратор — это шаблон проектирования, который позволяет изменять функциональность функции, оборачивая её в другую функцию.

Внешняя функция называется декоратором, она принимает исходную функцию в качестве аргумента и возвращает её изменённую версию.


Предварительные условия для изучения декораторов

Прежде чем изучать декораторы, нужно понять несколько важных концепций, связанных с функциями Python. Также помните, что в Python всё является объектом, включая функции.

Вложенная функция

Мы можем поместить одну функцию внутрь другой, это называется вложенной функцией. Например,

def outer(x):
    def inner(y):
        return x + y
    return inner

add_five = outer(5)
result = add_five(6)
print(result)  # выводит 11

# Вывод: 11

Здесь мы создали функцию inner() внутри функции outer().

Передача функции как аргумента

В Python мы можем передавать функцию в качестве аргумента другой функции. Например,

def add(x, y):
    return x + y

def calculate(func, x, y):
    return func(x, y)

result = calculate(add, 4, 6)
print(result)  # выводит 10

Вывод

10

В приведённом примере функция calculate() принимает функцию в качестве своего аргумента. При вызове calculate() мы передаём функцию add() как аргумент.

В функции calculate() аргументы: func, x, y становятся add, 4 и 6 соответственно.

И поэтому func(x, y) становится add(4, 6), что возвращает 10.

Возврат функции как значения

В Python мы также можем возвращать функцию в качестве возвращаемого значения. Например,

def greeting(name):
    def hello():
        return "Hello, " + name + "!"
    return hello

greet = greeting("Atlantis")
print(greet())  # выводит "Hello, Atlantis!"

# Вывод: Hello, Atlantis!

В приведённом примере оператор return hello возвращает внутреннюю функцию hello(). Эта функция теперь присваивается переменной greet.

Поэтому, когда мы вызываем greet() как функцию, получаем результат.


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

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

На самом деле любой объект, реализующий специальный метод __call__(), называется вызываемым. Так что в самом базовом смысле декоратор — это вызываемый объект, возвращающий вызываемый объект.

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

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

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

# Вывод: I am ordinary

Здесь мы создали две функции:

  • ordinary(), которая выводит "I am ordinary"

  • make_pretty(), которая принимает функцию в качестве аргумента, имеет вложенную функцию inner() и возвращает внутреннюю функцию.

Мы вызываем функцию ordinary() обычным способом, поэтому получаем вывод "I am ordinary". Теперь вызовем её через функцию-декоратор.

def make_pretty(func):
    # определяем внутреннюю функцию
    def inner():
        # добавляем дополнительное поведение к декорируемой функции
        print("I got decorated")

        # вызываем оригинальную функцию
        func()
    # возвращаем внутреннюю функцию
    return inner

# определяем обычную функцию
def ordinary():
    print("I am ordinary")

# декорируем функцию ordinary
decorated_func = make_pretty(ordinary)

# вызываем декорированную функцию
decorated_func()

Вывод

I got decorated
I am ordinary

В показанном выше примере make_pretty() — это декоратор. Обратите внимание на код:

decorated_func = make_pretty(ordinary)
  • Мы передаём функцию ordinary() как аргумент в make_pretty().

  • Функция make_pretty() возвращает внутреннюю функцию, которая теперь присвоена переменной decorated_func.

decorated_func()

Здесь мы фактически вызываем функцию inner(), где выводим текст.

Символ @ с декоратором

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

def make_pretty(func):

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

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

ordinary()

Вывод

I got decorated
I am ordinary

Здесь функция ordinary() декорируется декоратором make_pretty() с использованием синтаксиса @make_pretty, что эквивалентно вызову ordinary = make_pretty(ordinary).


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

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

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

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

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

def smart_divide(func):
    def inner(a, b):
        print("I am going to divide", a, "and", b)
        if b == 0:
            print("Whoops! cannot divide")
            return

        return func(a, b)
    return inner

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

divide(2,5)

divide(2,0)

Вывод

I am going to divide 2 and 5
0.4
I am going to divide 2 and 0
Whoops! cannot divide

Здесь, когда мы вызываем функцию divide() с аргументами (2,5), вместо неё вызывается функция inner(), определённая в декораторе smart_divide().

Эта функция inner() вызывает исходную функцию divide() с аргументами 2 и 5 и возвращает результат, который равен 0.4.

Аналогично, когда мы вызываем функцию divide() с аргументами (2,0), функция inner() проверяет, что b равно 0, и выводит сообщение об ошибке перед возвратом None.


Цепочки декораторов в Python

В Python можно объединять несколько декораторов в цепочки.

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

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

def percent(func):
    def inner(*args, **kwargs):
        print("%" * 15)
        func(*args, **kwargs)
        print("%" * 15)
    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
***************
%%%%%%%%%%%%%%%