装饰器(Decorator)是参数为被装饰函数(function as parameter)的可调用对象(callable object)。
class C:
def meth (cls):
...
meth = classmethod(meth) # Rebind name to wrapped-up class method
通过引入装饰器将上述代码包装为以下形式的等效代码:
class C:
@classmethod
def meth (cls):
...
严格来说,装饰器只是简化编程的语法糖。元编程(运行时改变程序的行为)中常用。
装饰器的关键特性之一是其在被装饰的函数定义之后立即运行,即导入时(即Python加载模块时)执行。
# BEGIN REGISTRATION
registry = [] # <1>
def register(func): # <2>
print('running register(%s)' % func) # <3>
registry.append(func) # <4>
return func # <5>
@register # <6>
def f1():
print('running f1()')
@register
def f2():
print('running f2()')
def f3(): # <7>
print('running f3()')
def main(): # <8>
print('running main()')
print('registry ->', registry)
f1()
f2()
f3()
if __name__=='__main__':
main() # <9>
# END REGISTRATION
借助装饰器实现注册功能:
$ python registration.py
running register(<function f1 at 0x000001F67FF974C0>)
running register(<function f2 at 0x000001F67FF97C40>)
running main()
registry -> [<function f1 at 0x000001F67FF974C0>, <function f2 at 0x000001F67FF97C40>]
running f1()
running f2()
running f3()
导入时执行:
>>> import registration
running register(<function f1 at 0x000002E8F38CA160>)
running register(<function f2 at 0x000002E8F38CA0C0>)
装饰器函数与被装饰函数往往不在同一模块中定义。装饰器定义后应用到其他模块中的函数上。
从装饰器的行为上看,装饰器可能会处理被装饰的函数然后将其返回,或者将其替换为另一个函数或可调用对象。
这类装饰器很重要的应用就是将函数注册到某一中心化数据结构中,如前文的REGISTRATION
模块。
利用装饰器将所有促销策略轻松地加入策略列表中,避免难以发现的代码遗忘引起的报错。
promos = [] # <1>
def promotion(promo_func): # <2>
promos.append(promo_func)
return promo_func
@promotion # <3>
def fidelity(order):
"""5% discount for customers with 1000 or more fidelity points"""
return order.total() * .05 if order.customer.fidelity >= 1000 else 0
@promotion
def bulk_item(order):
"""10% discount for each LineItem with 20 or more units"""
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * .1
return discount
@promotion
def large_order(order):
"""7% discount for orders with 10 or more distinct items"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * .07
return 0
def best_promo(order): # <4>
"""Select best discount available
"""
return max(promo(order) for promo in promos)
当装饰器返回新的内部定义的函数时,往往会形成高阶的嵌套函数,涉及闭包(closure)问题。
闭包指延伸了作用域的函数,其中包含在函数体外定义而在函数体内引用的非全局变量。只有在涉及嵌套函数时才会产生闭包问题。通过以下计算平均值的示例理解:
# class implementation
class Averager():
def __init__(self):
self.series = [] # save in class
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total/len(self.series)
"""
>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
"""
等效于以闭包方式(嵌套函数)实现的高阶函数:
# high-level function implementation
def make_averager():
series = [] # local variable in make_avg(), also free variable for average()
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)
return averager
"""
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
"""
高阶函数实现的最关键问题在于:average()
函数从哪里寻找存储历史值的series
数组呢?
审查返回的对象一窥玄机:
>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__ # doctest: +ELLIPSIS
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]
Python在__code__
属性中保存局部变量和自由变量的名称,该属性表示编译后的函数定义体。
series
绑定于avg的__closure__
属性。avg.__closure__
各元素对应于avg.__code__.co_freevars
的一个名称。这些绑定于闭包属性的元素均为cell
对象,有cell_contents
属性,保存历史值。
综上,闭包是一种保存自由变量的绑定的函数,从而在调用时使用定义作用域之外的绑定自由变量。
出于空间复杂性的考虑,计算均值的函数不必存储所有历史值,只需保存元素个数和总和两个变量即可。如果我们以这种思路轻易地编写代码便会出现缺陷:
def make_average():
count = 0
total = 0
def average(new_val):
count += 1
total += new_val
return total / count
return average
"""
>>> avg = make_average()
>>> avg(10)
Traceback (most recent call last):
...
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
"""
问题在于:count
与total
变量是不可变类型(与字典、列表等可变类型不同),在average()
内部赋值时会隐式创建局部变量副本,如此变量就不是自由变量,因而不能保存在闭包中。
为了解决这一问题,Python 3引入了nonlocal
声明。其作用是将变量标记为自由变量,无论是否赋予变量新值,均不会改变自由变量的事实。如果为声明后的变量赋予新值,那么闭包中保存的绑定也会更新。
def make_average():
count = 0
total = 0
def average(new_val):
nonlocal count, total
count += 1
total += new_val
return total / count
return average
"""
>>> avg = make_average()
>>> avg(10)
10.0
"""
Python 2没有
nonlocal
,因此在引入nonlocal
的PEP3104中的第3个代码片段给出了一种方法。这种处理方式是将内部函数需要修改的变量存储为可变对象(如字典或简单的实例)的元素或属性,并且把该对象绑定给一自由变量。
新定义的函数接受和被装饰函数相同的参数,在函数运行之外执行额外的动作,最后返回被装饰函数的执行结果。这是常用的装饰器编写策略。函数运行计时器就是这种编写方法的示例之一。
# clockdeco.py
import time
def clock(func):
def clocked(*args):
t0 = time.time()
result = func(*args)
elapsed = time.time() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked
if __name__ == '__main__':
@clock()
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
"""
$ python clockdeco.py
[0.12865448s] snooze(0.123) -> None
[0.12400031s] snooze(0.123) -> None
[0.12472081s] snooze(0.123) -> None
"""
装饰器能有助于检查某个人是否被授权去使用一个web应用的端点(endpoint)。它们被大量使用于Flask和Django web框架中。这里是一个例子来使用基于装饰器的授权:
from functools import wraps
def requires_auth(f):
@wraps(f)
def decorated(*args, kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
authenticate()
return f(*args, kwargs)
return decorated
日志是装饰器运用的另一个亮点。这是个例子:
from functools import wraps
def logit(func):
@wraps(func)
def with_logging(*args, kwargs):
print(func.__name__ + " was called")
return func(*args, kwargs)
return with_logging
@logit
def addition_func(x):
"""Do some math."""
return x + x
result = addition_func(4)
# Output: addition_func was called
# clockdeco_param.py
# BEGIN CLOCKDECO_PARAM
import time
DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}'
def clock(fmt=DEFAULT_FMT): # <1>
def decorate(func): # <2>
def clocked(*_args): # <3>
t0 = time.time()
_result = func(*_args) # <4>
elapsed = time.time() - t0
name = func.__name__
args = ', '.join(repr(arg) for arg in _args) # <5>
result = repr(_result) # <6>
print(fmt.format(**locals())) # <7>
return _result # <8>
return clocked # <9>
return decorate # <10>
if __name__ == '__main__':
@clock() # <11>
def snooze(seconds):
time.sleep(seconds)
for i in range(3):
snooze(.123)
# END CLOCKDECO_PARAM
"""
>>> snooze(.1) # doctest: +ELLIPSIS
[0.101...s] snooze(0.1) -> None
>>> clock('{name}: {elapsed}')(time.sleep)(.2) # doctest: +ELLIPSIS
sleep: 0.20...
>>> clock('{name}({args}) dt={elapsed:0.3f}s')(time.sleep)(.2)
sleep(0.2) dt=0.201s
"""
import functools
from clockdeco import clock
@functools.lru_cache() # <1>
@clock # <2>
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)
if __name__=='__main__':
print(fibonacci(6))
# BEGIN HTMLIZE
from functools import singledispatch
from collections import abc
import numbers
import html
@singledispatch # <1>
def htmlize(obj):
content = html.escape(repr(obj))
return '{}
'.format(content)
@htmlize.register(str) # <2>
def _(text): # <3>
content = html.escape(text).replace('\n', '
\n')
return '{0}
'.format(content)
@htmlize.register(numbers.Integral) # <4>
def _(n):
return '{0} (0x{0:x})
'.format(n)
@htmlize.register(tuple) # <5>
@htmlize.register(abc.MutableSequence)
def _(seq):
inner = '\n' .join(htmlize(item) for item in seq)
return '\n- '
+ inner + '\n'
# END HTMLIZE
r"""
htmlize(): generic function example
# BEGIN HTMLIZE_DEMO
>>> htmlize({1, 2, 3}) # <1>
'{1, 2, 3}
'
>>> htmlize(abs)
'<built-in function abs>
'
>>> htmlize('Heimlich & Co.\n- a game') # <2>
'Heimlich & Co.
\n- a game
'
>>> htmlize(42) # <3>
'42 (0x2a)
'
>>> print(htmlize(['alpha', 66, {3, 2, 1}])) # <4>
alpha
66 (0x42)
{1, 2, 3}
# END HTMLIZE_DEMO
"""
Fluent Python Chapter 7: Function Decorators and Enclosures(src: github/fluentpython/example-code/07-closure-deco)
Python Documentation: PEP318: Decorators for Functions and Methods & PEP3129: Class Decorators
Python Enhancement Proposals: PEP3104, PEP318 & PEP329
Python 函数装饰器 | 菜鸟教程