BugBug

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

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

Ситуация в которой могут потребоваться декораторы

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

Давайте начнем с очевидного решения проблемы:


import datetime
def func_x(args):
    do_some_a()
    do_some_b()
    do_some_c()
start=datetime.datetime.now()
func_x(real_args)
stop=datetime.datetime.now()
print("Время выполнения функции: {0}".format(stop-start))
		

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


import datetime
def func_x(args):
    start=datetime.datetime.now()
    do_some_a()
    do_some_b()
    do_some_c()
    stop=datetime.datetime.now()
    print("Время выполнения функции: {0}".format(stop-start))
		

Рассмотрим преимущества такого подхода:

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

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


import datetime
def func_x(args):
    start=datetime.datetime.now()
    do_some_a()
    do_some_b()
    do_some_c()
    stop=datetime.datetime.now()
    print("Время выполнения функции: {0}".format(stop-start))

def func_y(args):
    start=datetime.datetime.now()
    do_some_a()
    do_some_b()
    do_some_c()
    stop=datetime.datetime.now()
    print("Время выполнения функции: {0}".format(stop-start))
    
def func_z(args):
    start=datetime.datetime.now()
    do_some_a()
    do_some_b()
    do_some_c()
    stop=datetime.datetime.now()
    print("Время выполнения функции: {0}".format(stop-start))
		

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

Функции в Python возвращают функции

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


def func_x():
    print("Это функция func_x")
    def func_y():
        print("Это функция func_y")
        return 1
    print("Снова функция func_x")
    return func_y

print(func_y)   #A
x=func_x()      #B 
print(x)        #C
print(x())      #D 

		

Давайте разбираться как этот код работает. В ситуации A, при вызове функции func_y мы получим NameError ошибку. Она возникает поскольку данная функция не определена в глобальной области видимости. Она существует локально, только внутри функции func_x.

Код с комментарием B отобразит следующее:


Это функция func_x
Снова функция func_x
		
При данном вызове функция func_y не выполняется.

Обратимся к следующей строке с комментарием C. Результат её выполнения:


.func_y at 0x037CE930>
		
В данном случаи возвращаемое значение func_x само является функцией. Если вы попробуете снова выполнить строки B и C и обратите внимание на адрес возвращаемого значения, заметите, что каждый раз он разный. Каждый раз fucn_x создает новую функцию func_y.

Наконец строка C. Так как x является функцией, её можно вызвать. Вызов x вызывает экземпляр func_y, что дает следующий результат:


Это функция func_y
1
	

Надеюсь этот пример вас не очень запутал. Постарайтесь разобраться в нем это важно для понимания работы декораторов.

Назад к задаче синхронизации

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


import datetime
def time_stamp(func):                      			       # 1
    def new_func(*args,**kwargs):                                      # 2
        start=datetime.datetime.now()                                  # 3
        x=func(*args,**kwargs)                                         # 4
        stop=datetime.datetime.now()                                   # 3
        print("Время выполнения функции: {0}".format(stop-start))      # 3
        return x                                                       # 5
    return new_func()                                                  # 6
	

Давайте разберемся в каждой строке кода:

  1. В этой строке определяется функция time_stamp, принимающая один параметр.
  2. Внутри time_stamp мы определяем функцию. Каждый раз при вызове time_stamp она создается по-новой.
  3. Строки выполняющие функцию синхронизации времени.
  4. Вызывается передаваемая функции и сохраняется результат ее работы.
  5. Функция time_stamp должна действовать точно, так же как и переданная ей функция, поэтому возвращается результат ее работы.
  6. time_stamp функция возвращает переданную функцию.

Теперь мы можем использовать функцию time_stamp для вычисления скорости выполнения других функций:


def func_x(args):
    do_some_a()
    do_some_b()
    do_some_c()
    
time_func_x=time_stamp(func_x)

def func_y(args):
    do_some_a()
    do_some_b()
    do_some_c()
    
time_func_y=time_stamp(func_y)    
    
def func_z(args):
	do_some_a()
    do_some_b()
    do_some_c()

time_func_z=time_stamp(func_z) 

При выполнении time_func_x=time_stamp(func_x) в переменную time_func_x возвращается функция переданная time_stamp(). Получается что мы по-прежнему вызываем нужную нам функцию, но оборачиваем ее в дополнительный функционал. На мой взгляд это крайне интересный и функциональный инструмент языка.

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

Код выше прекрасно работает, но ужасно выглядит. Разработчики Python заботятся об эстетической стороне кода и поэтому создали красивую форму записи всего выше описанного:


@time_stamp
def func_y(args):
    do_some_a()
    do_some_b()
    do_some_c()
func_y
В точности соответствует:

def func_x(args):
    do_some_a()
    do_some_b()
    do_some_c()
    
time_func_x=time_stamp(func_x)	

Фактически декоратор мы написали в предыдущей части статьи, а тут просто предали общепринятую форму записи. Обычно это называют синтаксическим сахаром языка. В символе @ нет ни чего особенного. Это просто соглашение о сокращенной записи.

Таким образом, декоратор-это просто функция, которая возвращает функцию. Надеюсь статья была вам полезна, и вы поняли как они устроенны.