Python: задекорируем-ка декораторы. Снова

от автора

В прошлом году на Хабре уже была очень развёрнутая статья в двух частях о декораторах. Цель этой новой статьи — cut to the chase и сразу заняться интересными, осмысленными примерами, чтобы в конце концов разобраться в примерах ещё более мудрёных, чем в предыдущих статьях.
Целевая аудитория — программисты, уже знакомые (например по C#) с функциями высшего порядка и с замыканиями, но привыкшие, что аннотации у функций — это «метаинформация», проявляющаяся только при рефлексии. Особенность Питона, сразу же бросающаяся в глаза таким программистам — то, что присутствие декоратора перед объявлением функции позволяет изменить поведение этой функции:

Как это работает? Ничего хитрого: декоратор — это просто функция, принимающая аргументом декорируемую функцию, и возвращающая «исправленную»:

def timed(fn):     def decorated(*x):         start = time()         result = fn(*x)         print "Executing %s took %d ms" % (fn.__name__, (time()-start)*1000)         return result     return decorated  @timed def cpuload():     load = psutil.cpu_percent()     print "cpuload() returns %d" % load     return load  print "cpuload.__name__==" + cpuload.__name__ print "CPU load is %d%%" % cpuload() 

(Исходник целиком)

 cpuload.__name__==decorated cpuload() returns 16 Executing cpuload took 105 ms CPU load is 16% 

Объявление @timed def cpuload(): ... разворачивается в def cpuload(): ...; cpuload=timed(cpuload), так что в результате глобальное имя cpuload связывается с функцией decorated внутри timed, замкнутой на исходную функцию cpuload через переменную fn. В результате мы и видим cpuload.__name__==decorated

В качестве декоратора может использоваться любое выражение, значение которого — функция, принимающая функцию и возвращающая функцию. Таким образом возможно создавать «декораторы с параметрами» (фактически, фабрики декораторов):

def repeat(times):     """ повторить вызов times раз, и вернуть среднее значение """     def decorator(fn):         def decorated2(*x):             total = 0             for i in range(times):                 total += fn(*x)             return total / times         return decorated2     return decorator  @repeat(5) def cpuload():     """ внутри функции cpuload ничего не изменилось """  print "cpuload.__name__==" + cpuload.__name__ print "CPU load is %d%%" % cpuload() 

(Исходник целиком)

 cpuload.__name__==decorated2 cpuload() returns 7 cpuload() returns 16 cpuload() returns 0 cpuload() returns 0 cpuload() returns 33 CPU load is 11% 

Значение выражения repeat(5) — функция decorator, замкнутая на times=5. Это значение и используется в качестве декоратора; фактически имеем def cpuload(): ...; cpuload=repeat(5)(cpuload)

Можно сочетать несколько декораторов на одной функции, тогда они применяются в естественном порядке — справа налево. Если два предыдущих примера объединить в @timed @repeat(5) def cpuload(): — то на выходе получим

 cpuload.__name__==decorated cpuload() returns 28 cpuload() returns 16 cpuload() returns 0 cpuload() returns 0 cpuload() returns 0 Executing decorated2 took 503 ms CPU load is 9% 

А если поменять порядок декораторов — @repeat(5) @timed def cpuload(): — то получим

 cpuload.__name__==decorated2 cpuload() returns 16 Executing cpuload took 100 ms cpuload() returns 14 Executing cpuload took 109 ms cpuload() returns 0 Executing cpuload took 101 ms cpuload() returns 0 Executing cpuload took 100 ms cpuload() returns 0 Executing cpuload took 99 ms CPU load is 6% 

В первом случае объявление развернулось в cpuload=timed(repeat(5)(cpuload)), во втором случае — в cpuload=repeat(5)(timed(cpuload)). Обратите внимание и на печатаемые имена функций: по ним можно проследить цепочку вызовов в обоих случаях.

Предельный случай параметрической декорации — декоратор, принимающий параметром декоратор:

def toggle(decorator):     """ позволить "подключать" и "отключать" декоратор """     def new_decorator(fn):         decorated = decorator(fn)          def new_decorated(*x):             if decorator.enabled:                 return decorated(*x)             else:                 return fn(*x)          return new_decorated      decorator.enabled = True     return new_decorator  @toggle(timed) def cpuload():     """ внутри функции cpuload ничего не изменилось """  print "cpuload.__name__==" + cpuload.__name__ print "CPU load is %d%%" % cpuload() timed.enabled = False print "CPU load is %d%%" % cpuload() 

(Исходник целиком)

 cpuload.__name__==new_decorated cpuload() returns 28 Executing cpuload took 101 ms CPU load is 28% cpuload() returns 0 CPU load is 0% 

Значение, управляющее подключением/отключением декоратора, сохраняется в атрибуте enabled декорированной функции: Питон позволяет «налепить» на любую функцию произвольные атрибуты.

Получившуюся функцию toggle можно использовать и в качестве декоратора для декораторов:

@toggle def timed(fn):     """ внутри декоратора timed ничего не изменилось """  @toggle def repeat(times):     """ внутри декоратора repeat ничего не изменилось """  @timed @repeat(5) def cpuload():     """ внутри функции cpuload ничего не изменилось """  print "cpuload.__name__==" + cpuload.__name__ print "CPU load is %d%%" % cpuload() timed.enabled = False print "CPU load is %d%%" % cpuload() 

(Исходник целиком)

 cpuload.__name__==new_decorated cpuload() returns 28 cpuload() returns 0 cpuload() returns 0 cpuload() returns 0 cpuload() returns 0 Executing decorated2 took 501 ms CPU load is 5% cpuload() returns 0 cpuload() returns 16 cpuload() returns 14 cpuload() returns 16 cpuload() returns 0 Executing decorated2 took 500 ms CPU load is 9% 

Гм… нет, не сработало! Но почему?
Почему декоратор timed не отключился при втором вызове cpuload?

Вспомним, что глобальное имя timed у нас связано с декорированным декоратором, т.е. с функцией new_decorated; значит, строчка timed.enabled = False изменяет на самом деле атрибут функции new_decorated — общей «обёртки» обоих декораторов. Можно было бы внутри new_decorated вместо if decorator.enabled: проверять if new_decorator.enabled:, но тогда строчка timed.enabled = False будет отключать сразу оба декоратора.

Исправим этот баг: чтобы пользоваться атрибутом enabled на «внутреннем» декораторе, как и прежде — налепим на функцию new_decorated пару методов:

def toggle(decorator):     """ позволить "подключать" и "отключать" декоратор """     def new_decorator(fn):         decorated = decorator(fn)          def new_decorated(*x): # без изменений             if decorator.enabled:                 return decorated(*x)             else:                 return fn(*x)          return new_decorated      def enable():         decorator.enabled = True     def disable():         decorator.enabled = False     new_decorator.enable = enable     new_decorator.disable = disable     enable()     return new_decorator  print "cpuload.__name__==" + cpuload.__name__ print "CPU load is %d%%" % cpuload() timed.disable() print "CPU load is %d%%" % cpuload() 

(Исходник целиком)
Желаемый результат достигнут — timed отключился, но repeat продолжил работать:

 cpuload.__name__==new_decorated cpuload() returns 14 cpuload() returns 16 cpuload() returns 0 cpuload() returns 0 cpuload() returns 0 Executing decorated2 took 503 ms

 CPU load is 6% cpuload() returns 0 cpuload() returns 0 cpuload() returns 7 cpuload() returns 0 cpuload() returns 0 CPU load is 1% 

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

ссылка на оригинал статьи http://habrahabr.ru/post/187482/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *