Cat

Декораторы в питоне

Как перестать бояться декораторов и начать писать собственные.

Нюансы Python proDream 07 Декабрь 2023 Просмотров: 415

Вы наверняка сталкивались с декораторами в Python, но знаете ли вы, как они работают? Можете без подсказки написать свой декоратор? А если вы только начинаете изучать Python и ещё не знакомы с ними, то не за горами тот момент, когда вы с ними так или иначе встретитесь, так почему бы не ускорить встречу?

 

Небольшое теоретическое вступление.

Прежде чем мы поговорим о декораторах, рассмотрим один небольшой, но важный нюанс, касающийся функций. Функции, как и всё в Python, являются объектами. И, как и любой другой объект, мы можем передавать функцию в качестве аргумента в другую функцию. Рассмотрим это на следующем примере.

def func_one():
    print("Как дела?”)


def func_two(func):
    func()


func_two(func_one)  # Как дела?

 

Вот, что у нас здесь происходит:

1️⃣ Сначала мы создаем функцию func_one, которая выводит на печать строку “Как дела?”.
2️⃣Затем создаем вторую функцию func_two. Она принимает в качестве аргумента некоторую функцию func, которую в дальнейшем вызывает. Обратим внимание на то, что для передачи функции в качестве аргумента мы пишем только название функции - func, но не ставим скобки, так как в этот момент не вызываем ее.
3️⃣ Последним действием вызовем функцию func_two, передав в нее func_one. Как мы видим, func_one успешно вызывается внутри func_two, о чем и возвещает, интересуясь, как у нас дела.

 

Функции-обертки.

Давайте немного усложним рассматриваемую конструкцию:

def deco(func):
    def wrapper():
        print("Привет!")
        func()
        print("Пока!")

    return wrapper


def func_one():
    print("Как дела?")


deco(func_one)

 

1️⃣ Создаем функцию deco, в которую передадим в качестве аргумента некоторую функцию func.
2️⃣ Внутри функции deco создаем еще одну функцию wrapper. В ней сначала выводим на печать приветствие (“Привет!”), затем вызываем функцию func, которую получаем из внешней функции deco при помощи замыкания (обращения к объекту из внешней области видимости). Далее выводим на печать прощание (“Пока!”).
3️⃣ Функция deco возвращает функцию wrapper.
4️⃣ Создаем уже известную нам функцию func_one, вопрошающую как дела.
5️⃣ Вызываем функцию deco.

Что же мы получим? Ничего. Всё дело в том, что функция deco возвращает функцию-обёртку wrapper, но не вызывает её. Чтобы заставить всю конструкцию работать, нам надо сделать кое-что странное: поставить после вызова функции deco дополнительную пару скобок.

deco(func_one)()

# Привет!
# Как дела?
# Пока!

 

Вот теперь у нас всё заблестело и заискрилось: функция wrapper, которую вернула нам вызванная функция deco, вызвалась дополнительной парой скобок и подарила нам короткий, но вежливый монолог: “Привет! Как дела? Пока!”.

 

А где же декораторы?

Терпение, спокойствие, сейчас они появятся.
Как нам сделать так, чтобы вызов функции func_one всегда сопровождался оборачиванием ее в функцию wrapper? Другими словами, как нам отдекорировать func_one, чтобы до вопроса о делах с нами здоровались, а после – прощались?

Решение как будто напрашивается: нам надо перезаписать в переменную func_one результат вызова функции deco с переданной в нее в качестве аргумента изначальной функцией func_one:

func_one = deco(func_one)

Еще раз: у нас была функция func_one. Мы обернули ее дополнительным кодом при помощи функции deco и в переменную func_one записали уже результат этого оборачивания. Теперь когда бы мы ни вызвали func_one, мы всегда получим усовершенствованный (отдекорированный) результат ее работы:

func_one()

# Привет!
# Как дела?
# Пока!

А есть ли более удобный вариант записи для вызова функции декоратора? Есть - при помощи символа @.

Интересный факт: символ @, по мнению разработчиков, похож на пирог, от чего такой способ вызова декоратора назвали "Pie Decorator Syntax"

 

Полностью наш код будет выглядеть следующим образом:

def deco(func):
    def wrapper():
        print("Привет!")
        func()
        print("Пока!")

    return wrapper


@deco
def func_one():
    print("Как дела?")


func_one()

# Привет!
# Как дела?
# Пока!

 

Результат работы этого кода точно такой же, просто синтаксис стал попроще и посимпатичнее. У большинства само понятие «декоратор» плотно ассоциируется с этим «пирожковым» оператором, но не самом деле декорирование - это не про синтаксис. Декорирование - это про принцип.

А что делать, если нам надо отдекорировать функцию, которая принимает какие-то аргументы?
В этой ситуации на помощь приходят звёздные братья *args и **kwargs. Многих начинающих питонистов эта парочка пугает, но на самом деле бояться здесь нечего. Это всего лишь синтаксис, который позволяет нам не уточнять, сколько аргументов мы хотим передать в функцию:

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

def my_function(*args, **kwargs):
    print(args[1])
    print(kwargs[name])

my_function(1, 2, 3, name='John', age=25)

def my_function(*args, **kwargs):
    print(args[1])
    print(kwargs['name'])

my_function(1, 2, 3, name='Аристарх', age=25)

# 2
# Аристарх

 

Мы передали в функцию my_function заранее неоговоренное количество позиционных (1, 2, 3) и именованных (name="Аристарх", age=25) аргументов. В самой функции по индексу 1 мы вызвали второй элемент кортежа позиционных аргументов (2) и по ключу "name" вызвали из словаря значение ("Аристарх"). Ничего сложного. Надо только следить за тем, чтобы в функции не вызывался аргумент с несуществующим индексом в кортеже или с несуществующим ключом в словаре. Иначе работа кода прервется исключением. Исключение, правда, можно и обработать, но об этом поговорим как-нибудь в другой раз.

 

Вернёмся к нашим декораторам.

Допустим, что строку в нашей функции func мы не прописываем уже внутри, а передаем в функцию позиционным аргументом. Тогда уже знакомый нам код изменится следующим образом:

def deco(func):
    def wrapper(*args, **kwargs):
        print("Привет!")
        func(*args, **kwargs)
        print("Пока!")

    return wrapper


@deco
def func_one(text):
    print(text)


func_one("Как дела?")

# Привет!
# Как дела?
# Пока!

 

1️⃣ В функцию-обертку (wrapper) мы записываем те самые *args и **kwargs
2️⃣ В функцию func внутри wrapper мы также их передаём
3️⃣ В декорируемую функцию передаем аргумент text

В итоге всё работает ровно так, как мы и хотели, но функция и декоратор приобрели более универсальный вид.

 

Так что же такое декораторы?

Декоратор - это функция, которая позволяет нам "обернуть” кодом другую функцию, не изменяя при этом код самой оборачиваемой функции. Это даёт нам возможность выполнить какие-то дополнительные действия до и/или после выполнения самой оборачиваемой функции. Например, как в нашем примере выше, поздороваться перед тем, как сказать что-то, и попрощаться после этого. В принципе декораторы похожи на ритуалы, которые мы все выполняем в обычной жизни. Например, перед тем, как что-то съесть, мы моем руки. При этом неважно, из чего будет состоять трапеза: перед едой все хорошие мальчики и девочки обязательно вымоют руки - таков декоратор, обернувший функцию “поесть” в процессе воспитания в детстве.

Самым распространённым примером использования декоратора в коде является логгер. Он срабатывает при выполнении декорируемой функции и записывает в файл результат выполнения, а также сопутствующие данные, такие как название функции, входные аргументы и другие.

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

Автор

    Нет комментариев

    Реклама