装饰器的本质很简单,它本质是一个函数,入参是另一个函数:
@decorate
def target():
pass
# 上面的写法等价于
def target():
pass
target = decorate(target)
被装饰过的函数不一定是原先的函数,有可能是被替换过之后的函数,但更为常见的场景是在原有函数的基础上完成了一些额外的工作。
另外需要注意的是,装饰器的执行时间,它的执行时间和其他定义的函数不同。装饰器在被装饰函数定义之后立即执行,通常可以认为是导入时执行。
registry = []
def register(func):
print('running register(%s)' % func)
registry.append(func)
return func
@register
def f1():
print('running f1()')
@register
def f2():
print('running f2()')
def f3():
print('running f3()')
def main():
print('running main()')
print('registry ->', registry)
f1()
f2()
f3()
if __name__=='__main__':
main()
执行上面代码:
>>> python3 registration.py
running register(<function f1 at 0x100631bf8>)
running register(<function f2 at 0x100631c80>)
running main()
registry -> [<function f1 at 0x100631bf8>, <function f2 at 0x100631c80>]
running f1()
running f2()
running f3()
许多Python web框架(例如flask)通常会使用装饰器来完成路由注册的工作,除了将URL和函数绑定起来之外,装饰器在这里并不对函数做替换。
之前提高过的策略模式,在best_promo
函数那里,有时会因为没有注册新的策略导致最佳策略结果有误,而注册管理这种事情和打折策略本身又没有关系,属于额外的工作,非常适合使用装饰器来解决:
promos = []
def promotion(promo_func):
promos.append(promo_func)
return promo_func
@promotion
def fidelity(order):
"""为积分为1000或以上的顾客提供5%折扣"""
return order.total() * .05 if order.customer.fidelity >= 1000 else 0
@promotion
def bulk_item(order):
"""单个商品为20个或以上时提供10%折扣"""
discount = 0
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * .1
return discount
@promotion
def large_order(order):
"""订单中的不同商品达到10个或以上时提供7%折扣"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * .07
return 0
def best_promo(order): ➍
"""选择可用的最佳折扣
"""
return max(promo(order) for promo in promos)
这里的装饰器起到了注册策略的作用,如果想要禁用某个策略,简单的把那个函数的装饰器去掉就可以了。
接下来简单说一下闭包的概念。闭包本质是对函数作用域的延展,它包含函数定义体中引用、但并不在定义体中定义的非全局变量。
简单来说就是,在两个嵌套函数的场景下,内层函数可以访问定义在内外层函数之间的变量。
比如下面的例子:
def make_averager():
series = []
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
在上面的averager
函数中,series
是自由变量,它是指在本地作用域中绑定的变量。

那么,怎么知道函数有哪些自由变量呢?
>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]
所以,闭包实质是函数,它保留定义函数时的自由变量的绑定,调用函数时,虽然定义作用域不可用,但是还有绑定可以使用。
在Python3中,nonlocal
用于声明自由变量:
def make_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager
在常见初级python面试中,关于python装饰器常常会要求实现一个装饰器,打印被装饰方法的执行耗时:
def clock(func):
import time
def clocked(*args):
t0 = time.time()
result = func(*args)
t1 = time.time()
elapsed = t1 - 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
改进一下上面的装饰器,让它支持关键字参数:
def clock(func):
import time
import functools
@functools.wraps(func)
def clocked(*args, **kwargs):
t0 = time.time()
result = func(*args, **kwargs)
t1 = time.time()
elapsed = t1 - t0
name = func.__name__
arg_lst = []
if args:
arg_lst.append(', '.join(repr(arg) for arg in args))
if kwargs:
pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
arg_lst.append(', '.join(pairs))
arg_str = ', '.join(arg_lst)
print('[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result))
return result
return clocked
- 使用functools.lru_cache做缓存
functools.lru_cache实现了备忘的功能,LRU(Least Recently Used)表明备忘的数量不会无限增长,过一段时间或者超过一定数量就会被清除。
下面是斐波那契数列的例子:
from clockdeco import clock
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)
if __name__=='__main__':
print(fibonacci(6))
# 运行上面代码
>>> python3 fibo_demo.py
[0.00000095s] fibonacci(0) -> 0
[0.00000095s] fibonacci(1) -> 1
[0.00007892s] fibonacci(2) -> 1
[0.00000095s] fibonacci(1) -> 1
[0.00000095s] fibonacci(0) -> 0
[0.00000095s] fibonacci(1) -> 1
[0.00003815s] fibonacci(2) -> 1
[0.00007391s] fibonacci(3) -> 2
[0.00018883s] fibonacci(4) -> 3
[0.00000000s] fibonacci(1) -> 1
[0.00000095s] fibonacci(0) -> 0
[0.00000119s] fibonacci(1) -> 1
[0.00004911s] fibonacci(2) -> 1
[0.00009704s] fibonacci(3) -> 2
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00002694s] fibonacci(2) -> 1
[0.00000095s] fibonacci(1) -> 1
[0.00000095s] fibonacci(0) -> 0
[0.00000095s] fibonacci(1) -> 1
[0.00005102s] fibonacci(2) -> 1
[0.00008917s] fibonacci(3) -> 2
[0.00015593s] fibonacci(4) -> 3
[0.00029993s] fibonacci(5) -> 5
[0.00052810s] fibonacci(6) -> 8
8
不难看出,这里出现了大量的重复计算。这也是递归的一个特点:递归基会被多次重复调用。
接下来看一下lru_cache
的优化版本:
import functools
from clockdeco import clock
@functools.lru_cache()
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)
if __name__=='__main__':
print(fibonacci(6))
# 运行上面代码
>>> python3 fibo_demo_lru.py
[0.00000119s] fibonacci(0) -> 0
[0.00000119s] fibonacci(1) -> 1
[0.00010800s] fibonacci(2) -> 1
[0.00000787s] fibonacci(3) -> 2
[0.00016093s] fibonacci(4) -> 3
[0.00001216s] fibonacci(5) -> 5
[0.00025296s] fibonacci(6) -> 8
可以看到,lru_cache
方法减少了重复运算,从而带来了运行时间的减少。
接下来,简单说一下singledispatch
这个装饰器(之前有篇文章介绍过)。
简单来说,functools.singledispatch
主要用于将整体方案拆分成多个模块,用于应对多种情况,减少长串的if...elif...elif......
的代码块。被它装饰的函数会变成泛函数(generic function):根据第一个参数的类型,以不同方式执行相同操作的同一组函数。
下面展示带参数的装饰器。
registry = set()
def register(active=True):
def decorate(func):
print('running register(active=%s)->decorate(%s)'% (active, func))
if active:
registry.add(func)
else:
registry.discard(func)
return func
return decorate
@register(active=False)
def f1():
print('running f1()')
@register()
def f2():
print('running f2()')
def f3():
print('running f3()')
# 使用上面代码
>>> import registration_param
running register(active=False)->decorate(<function f1 at 0x10063c1e0>)
running register(active=True)->decorate(<function f2 at 0x10063c268>)
>>> registration_param.registry
{<function f2 at 0x10063c268>}
网友评论