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

Как это работает? Ничего хитрого: декоратор — это просто функция, принимающая аргументом декорируемую функцию, и возвращающая «исправленную»:
В качестве декоратора может использоваться любое выражение, значение которого — функция, принимающая функцию и возвращающая функцию. Таким образом возможно создавать «декораторы с параметрами» (фактически, фабрики декораторов):
Можно сочетать несколько декораторов на одной функции, тогда они применяются в естественном порядке — справа налево. Если два предыдущих примера объединить в
Предельный случай параметрической декорации — декоратор, принимающий параметром декоратор:
Получившуюся функцию
Почему декоратор
Вспомним, что глобальное имя
Исправим этот баг: чтобы пользоваться атрибутом
Желаемый результат достигнут —
Целевая аудитория — программисты, уже знакомые (например по 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%
