В прошлом году на Хабре уже была очень развёрнутая статья в двух частях о декораторах. Цель этой новой статьи — 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%
Это одна из очаровательнейших возможностей Питона — к функциям можно добавлять не только атрибуты, но и произвольные функции-методы. Функции на функциях сидят и функциями погоняют.
Автор: tyomitch