И снова про декораторы в Python

в 11:01, , рубрики: decorator, python, метки: ,

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

В системном администрировании если в резюме указано, что-то наподобии: «Знание стека протоколов TCP/IP», то жди вопроса: «Что такое маска подсети» и вдогонку предложение рассчитать маску. Клянусь вам на каждом собеседовании где я общался с техническим специалистом так было и даже когда уже мое время пришло проводить интервью на данную позицию, что вы думаете, я так же спрашивал про маски, когда речь заходила о сетях. Но самое интересное не это, а то что 50% кандидатов с трудом давали определение маски и только 20% из этой половины могли производить с ней базовые операции, а у многих в резюме вертелись всевозможные CCNA и прочие пугающие буквы.
Время шло я понял, что администрирование не мое и со спокойной душой ушел в python разработчики, но какого же было мое удивление, когда и там нашлись свои «маски подсети», а именно декораторы и ситуация с ними ничем не лучше.

И так в рамках данной статьи я хотел бы объяснить начинающим или не очень, что же такое декораторы. Сразу прошу отнестись с понимаем «гуру», цель статьи дать направление развития.

Итак, что же такое декоратор. Проще говоря это функция которая принимает другую функцию, попутно совершая над ней некое действие.
Форма записи для декоратора это:

@decor
def foo():
    print 'Spam'

, где @decor это указание на то что функция foo обернута декоратором decor.
Кстати определение декоратора как обертки-фантика на мой взгляд крайне удобно для человека который только начинает знакомиться с декораторами.
И в потверждение этого я приведу эквивалент записи декоратора:

f = decor(foo)

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

from time import time

def timer(foo):
	def _timer():
		t_before = time()
		foo()
		t_after = time()
		d_t = t_after — t_before
		print d_t
	return _timer

, а теперь обернем в декоратор функцию spam единственное назначение которой печатать слово spam

@timer
def spam():
	print 'spam'

if __name__ == '__main__':
	spam()

>>spam
>>4.88758087158e-05

Давайте подробнее разберемся что же происхоит в функции timer. В качестве аргумента данная функция принимает любую функцию и связывает ее с именем foo. Сам декоратор состоит из внешней функции timer и внутренней функции _timer.
Что происходит во внутренней функции?
В ней мы объявляем две переменные t_before и t_after в которых будут храниться как не сложно догадаться значение времени до вызова функции передаваемой в наш декоратор и после.
Самое на мой взгляд сложное в декораторе это строка return _timer, первый вопрос который напрашивается почему мы возвращаем _timer, а не _timer() и что это собственно такое.
Надо понимать, что в Python все является объектом, литеральное же обозначение объекта это ссылка на объект в памяти, то есть _timer это просто ссылка на участок памяти в котором хранится исполняемая функция. Зная это мы можем понять, что произойдет если мы вернем результат вызова функции _timer(), а собственно ничего, так как функция _timer сама по себе не имеет оператора return, поэтому мы получим исключение:

Traceback (most recent call last): 
  File "Downloads/d.py", line 15, in <module> 
    spam() 
TypeError: 'NoneType' object is not callable

все верно, грубо говоря «ничто» нельзя вызвать.

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

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

def printer(s):
	print s

очевидно что наш декоратор не сможет вызвать функцию printer так как для ее вызова требуется передать один аргумент. Следовательно надо переделать декоратор. Мы могли бы сделать для функции printer отдельный декоратор:

def timer(foo):
	def _timer(s):
		t_before = time()
		foo(s)
		t_after = time()
		d_t = t_after — t_before
		print d_t
	return _timer

, но тогда получается, что под каждую функцию надо писать отдельный декоратор в зависимости от того сколько и какие аргументы она принимает, согласитесь это совсем не удобно, поэтому тут мы будем использовать «магические» переменные *args и **kwrgs и перепишем наш декоратор так:

def timer(foo):
	def _timer(*args, **kwargs):
		t_before = time()
		foo(*args, **kwargs)
		t_after = time()
		d_t = t_after — t_before
		print d_t
	return _timer

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

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

Если хабрособществу будет интересно, то в следующей статье мне хотелось бы показать как написать класс декоратор и более подробно описать принцип работы данного инструмента.

Автор: wiklow

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js