Целевая аудитория — программисты, уже знакомые (например по 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 msCPU 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/
Добавить комментарий