Pandora ResearchPandora
Research
RUEN
Python

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

Pandora ResearchPandora Research
26 июня 20268 мин чтения

Перед тем как перейти к теме декораторов, нам нужно ввести два понятия — функции высшего порядка и замыкание функций. Далее будет немного теории, она несложная, и её понимание даст возможность работать с декораторами.

Функции высшего порядка

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

Основная идея состоит в том, что функции имеют тот же статус, что и другие объекты данных.

Мы можем передавать функции как параметры:

Python
def my_func(inside_func):
    ...
    inside_func()  # Вызов функции принятой в качестве аргумента
    ...

Можем возвращать функцию как результат:

Python
def a():
    def b():
        pass
    return b

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

Сделаем функцию, которая будет выполнять принимаемую функцию дважды:

Python
def twice_func(inside_func):
   """Функция, выполняющая дважды функцию принятую в качестве аргумента"""
   inside_func()
   inside_func()


def hello():
   print("Hello")


test = twice_func(hello)
# Hello
# Hello

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

Замыкание функций

Замыкание в программировании — это функция, в теле которой присутствуют ссылки на переменные, объявленные вне тела этой функции в окружающем коде и не являющиеся её аргументами.

Вспомните нелокальную (nonlocal) область видимости — на этом принципе работает замыкание функций.

Сделаем функцию, которая будет возвращать функцию, всегда прибавляющую одно и то же число x:

Python
def make_adder(x):
   def adder(n):
       return x + n # захват переменной "x" из nonlocal области
   return adder  # возвращение функции в качестве результата

То есть мы сделали конвейер, который будет прибавлять фиксированное число x к любому числу n (см. функции высшего порядка). Тогда сделаем функцию, которая будет прибавлять число 5 к любому числу.

Python
# функция, которая будет к любому числу прибавлять пятёрку
add_5 = make_adder(5)
print(add_5(10))  # 15
print(add_5(100))  # 105

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

Декораторы

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

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

Устройство простейшего декоратора

На рисунке выше представлен простейший декоратор, который не выполняет никаких дополнительных действий.

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

Давайте посмотрим на примере, как добавить дополнительное поведение к основной функции.

Python
def my_decorator(a_function_to_decorate):
    # Здесь мы определяем новую функцию - «обертку». Она нам нужна, чтобы выполнять
    # каждый раз при вызове оригинальной функции, а не только один раз
    def wrapper():
        # здесь поместим код, которые будет выполняться до вызова, потом вызов
        # оригинальной функции, потом код после вызова
        print("Я буду выполнен до основного вызова!")

        result = a_function_to_decorate()  # не забываем вернуть значение исходной функции

        print("Я буду выполнен после основного вызова!")
        return result
    return wrapper

Наш декоратор будет печатать строку «Я буду выполнен до основного вызова!» перед основным вызовом, а строку «Я буду выполнен после основного вызова!» — соответственно после основного вызова. Если необходимо, не забываем вернуть результат исходной функции.

Python
def my_function():
   print("Я - оборачиваемая функция!")
   return 0


print(my_function())
# Я - оборачиваемая функция!
# 0


decorated_function = my_decorator(my_function)  # декорирование функции
print(decorated_function())
# Я буду выполнен до основного вызова!
# Я - оборачиваемая функция!
# Я буду выполнен после основного вызова!
# 0

Видим, что, задекорировав my_function, мы добавили к ней новый функционал, не меняя исходный код самой функции.

Зачем это нужно? Например, декораторы могут замерять время выполнения функции либо количество запусков конкретной функции, также можно сохранять результаты вычислений (кеширование).

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

Python
import time


def decorator_time(fn):
   def wrapper():
       print(f"Запустилась функция {fn}")
       t0 = time.time()
       result = fn()
       dt = time.time() - t0
       print(f"Функция выполнилась. Время: {dt:.10f}")
       return dt  # задекорированная функция будет возвращать время работы
   return wrapper


def pow_2():
   return 10000000 ** 2


def in_build_pow():
   return pow(10000000, 2)


pow_2 = decorator_time(pow_2)
in_build_pow = decorator_time(in_build_pow)

pow_2()
# Запустилась функция <function pow_2 at 0x7f938401b158>
# Функция выполнилась. Время: 0.0000011921

in_build_pow()
# Запустилась функция <function in_build_pow at 0x7f938401b620>
# Функция выполнилась. Время: 0.0000021458

Чтобы замерить время, будем использовать модуль time, в котором есть функция time(), возвращающая текущее время. Зная время до начала выполнения и сразу после, можно вычислить время работы функции.

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

«Синтаксический сахар»

Декораторы — настолько часто используемая конструкция в Python, что её оформили в качестве «синтаксического сахара».

Синтаксический сахар в языке программирования — это синтаксические возможности, применение которых не влияет на поведение программы, но делает использование языка более удобным для человека.

Пользоваться им можно так:

Python
@my_decorator
def my_function():
    pass

При этом будет происходить всё то же самое, аналогичное.

Python
my_function = my_decorator(my_function)
`@my_decorator` над функцией — это ровно то же самое, что `my_function = my_decorator(my_function)`.

Имейте в виду, что при использовании синтаксического сахара на месте декорируемой функции появляется задекорированная функция!

Python
def my_decorator(fn):
   def wrapper():
       fn()
   return wrapper  # возвращается задекорированная функция, которая заменяет исходную


# выведем незадекорированную функцию
def my_function():
   pass
print(my_function)  # <function my_function at 0x7f938401ba60>


# выведем задекорированную функцию
@my_decorator
def my_function():
   pass
print(my_function)  # <function my_decorator.<locals>.wrapper at 0x7f93837059d8>

Видим, что после декорирования под названием исходной функции будет не сама функция, а функция, которая была внутри декоратора, — в данном случае функция wrapper.

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

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

Python
def do_it_twice(func):
    def wrapper():
        func(arg)
        func(arg)
    return wrapper


@do_it_twice
def say_word(word):
    print(word)


say_word("Oo!!!")

При выполнении данного кода будет выведена такая ошибка:

Python
TypeError: wrapper() takes 0 positional arguments but 1 was given

Связана она с тем, что задекорированная функция wrapper не принимает ни одного аргумента в отличие от исходной. А именно задекорированная функция теперь скрывается за именем переменной say_word. И каждый следующий вызов say_word — это вызов wrapper. Следовательно, wrapper должен уметь принимать те же аргументы, что и исходная функция, и передавать их в неё. Чтобы не задумываться над количеством аргументов и сделать наш декоратор универсальным, будем использовать *args и **kwargs.

Python
# декоратор, в котором встроенная функция умеет принимать аргументы
def do_it_twice(func):
   def wrapper(*args, **kwargs):
       func(*args, **kwargs)
       func(*args, **kwargs)
   return wrapper


@do_it_twice
def say_word(word):
   print(word)


say_word("Oo!!!")
# Oo!!!
# Oo!!!

Подведём итог по декораторам:

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

Декораторы — это вызовы дополнительных функций, поэтому они немного замедляют ваш код.

Для передачи аргументов декорируемой функции используйте *args и **kwargs.

Вот универсальный шаблон для декоратора:

Python
def my_decorator(fn):
    print("Этот код будет выведен один раз в момент декорирования функции")
    def wrapper(*args, **kwargs):
        print('Этот код будет выполняться перед каждым вызовом функции')
        result = fn(*args, **kwargs)
        print('Этот код будет выполняться после каждого вызова функции')
        return result
    return wrapper
Python

Есть задача для нашей команды?

Расскажите о проекте — оценим и предложим решение в течение одного рабочего дня.

Обсудить проект