内容来自流畅的python
虽然是python基础,但是看的时候感觉有种恍然大悟的感觉。
函数装饰器用于在源码中“标记”函数,以某种方式增加函数的行为。这是一项强大的功能,但是若想要掌握,必须理解闭包。
除了在装饰器中有用处之外,闭包还是回调异步编程和函数式编程风格的基础。
◆ ◆ ◆ ◆ ◆
装饰器是可调用的对象,其参数是另一函数(被装饰的函数)。
假如有一个名为decorate的装饰器
1@decorate
2def target**():
3 print('runing target()')
上述代码的效果与下述写法一样:
1def target():
2 print('running target()')
3
4target = decorate(target)
两个代码执行完之后的结果都为decorate(target)返回的内容。
使用装饰器把函数替换成另一个函数
定义一个装饰器deco返回inner函数对象
使用deco装饰target
1def deco(func):
2 def inner():
3 print('running inner()')
4 return inner
5
6@deco
7def target():
8 print('running target()')
下面进行结果输出:
调用被装饰的target其实会运行inner
1>>> target()
2running inner()
3
4>>> print(target)
5<function deco.<locals>.inner at 0x104549510>
装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时(即Python加载模块时)
1registry = []
2
3def register(func):
4 print('running register(%s)'%func)
5 registry.append(func)
6 return func
7
8@register
9def f1():
10 print('running f1()')
11
12@register
13def f2():
14 print('running f2()')
15
16def f3():
17 print('running f3()')
18
19if __name__ == '__main__':
20 print('----running----')
21 print('registry ->',registry)
22 f1()
23 f2()
24 f3()
执行结果如下:
running register()
running register()
----running----
registry -> [,]
running f1()
running f2()
running f3()
从结果可以看出,register在模块中其他函数之前运行了两次。调用register时,传给他的参数是被装饰的函数,例如
图片 1.png
在其他文件中导入的话可看到结果
图片 2.png
1>>> test.registry
2[<function f1 at 0x103d49510>, <function f2 at 0x10d09f400>]
综上所述:函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。
定义一个装饰器promotion用于给列表promos存储内容。
充分利用了装饰器的执行顺序。
优点:
1promos = []
2
3
4def promotion(promo_func):
5 promos.append(promo_func)
6 return promo_func
7
8@promotion
9def fidelity(order):
10 '''为积分1000或以上的顾客提供5%的折扣'''
11 return order.total() * 0.5 if order.customer.fidelity >= 1000 else 0
12
13@promotion
14def bulk_item(order):
15 '''单个商品20个或以上时提供10%的折扣'''
16 discount = 0
17 for item in order.cart:
18 if item.quantity >= 20:
19 discount += item.total() * .1
20 return discount
21
22@promotion
23def large_order(order):
24 '''订单中的不同商品达到10个或以上的时候提供4%的折扣'''
25 distinct_items = {item.product for item in order.cart}
26 if len(distinct_items) >= 10:
27 return order.total() * .04
28 return 0
29
30def best_promo(order):
31 '''选择可用的最佳折扣'''
32 return max(promo(order) for promo in promos)
一个简单的装饰器,输出函数的运行时间
1import time
2
3def clock(func):
4 def clocked(*args): # 接受任意个定位参数
5 t0 = time.perf_counter() # 返回系统运行时间
6 result = func(*args)
7 elapsed = time.perf_counter() - t0
8 name = func.__name__
9 arg_str = ','.join(repr(arg) for arg in args)
10 print('[%0.8fs]%s(%s) -> %r' % (elapsed, name, arg_str, result))
11 return result
12 return clocked
该函数实现了
装饰器的典型行为:把装饰的函数替换成为新函数,二者接受相同的参数,而且(通常)返回被装饰的函数本该返回的值,同时还会做一些额外操作。
1import time
2from test import clock
3
4
5@clock
6def snooze(seconds):
7 time.sleep(seconds)
8
9@clock
10def factorial(n):
11 '''doc test'''
12 return 1 if n < 2 else n * factorial(n - 1)
13
14if __name__ == '__main__':
15 print('*' * 20, 'Calling snooze(.123)')
16 snooze(.123)
17 print('*' * 20, 'Calling factorial(6)')
18 print('6 != ', factorial(6))
19
20结果:
21******************** Calling snooze(.123)
22[0.12652330s]snooze(0.123) -> None
23******************** Calling factorial(6)
24[0.00000174s]factorial(1) -> 1
25[0.00003119s]factorial(2) -> 2
26[0.00007065s]factorial(3) -> 6
27[0.00008813s]factorial(4) -> 24
28[0.00010634s]factorial(5) -> 120
29[0.00017910s]factorial(6) -> 720
306 != 720
但是会发现我们无法看到被装饰的函数的name__和__doc属性
1print(factorial.__doc__)# None
2
3print(factorial.__name__)# clocked
所以对上文中的clock进行一定的修改,使其支持关键字还有name__和__doc属性
1import time
2import functools
3
4def clock(func):
5 @functools.wraps(func)
6 def clocked(*args, **kwargs):
7 t0 = time.time()
8 result = func(*args, **kwargs)
9 elapsed = time.time() - t0
10 name = func.__name__
11 arg_lst = []
12 if args:
13 arg_lst.append(','.join(repr(arg) for arg in args))
14 if kwargs:
15 pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
16 arg_lst.append(','.join(pairs))
17 arg_str = ','.join(arg_lst)
18 print('[%0.8fs]%s(%s) -> %r' % (elapsed, name, arg_str, result))
19 return result
20 return clocked
functools.lru_cache实现了备忘功能,它能把耗时的函数的结果保存起来,避免传入相同的参数时重复计算
使用常规思路写一个斐波纳切数
1from test import clock
2
3@clock
4def fibonacci(n):
5 if n < 2:
6 return n
7 return fibonacci(n - 2) + fibonacci(n - 1)
8
9if __name__ == '__main__':
10 print(fibonacci(6))
11
12结果如下:
13[0.00000000s]fibonacci(0) -> 0
14[0.00000000s]fibonacci(1) -> 1
15[0.00004888s]fibonacci(2) -> 1
16[0.00000119s]fibonacci(1) -> 1
17[0.00000000s]fibonacci(0) -> 0
18[0.00000000s]fibonacci(1) -> 1
19[0.00001693s]fibonacci(2) -> 1
20[0.00003314s]fibonacci(3) -> 2
21[0.00010300s]fibonacci(4) -> 3
22[0.00000095s]fibonacci(1) -> 1
23[0.00000000s]fibonacci(0) -> 0
24[0.00000000s]fibonacci(1) -> 1
25[0.00001621s]fibonacci(2) -> 1
26[0.00003195s]fibonacci(3) -> 2
27[0.00000095s]fibonacci(0) -> 0
28[0.00000000s]fibonacci(1) -> 1
29[0.00001597s]fibonacci(2) -> 1
30[0.00000000s]fibonacci(1) -> 1
31[0.00000095s]fibonacci(0) -> 0
32[0.00000095s]fibonacci(1) -> 1
33[0.00001597s]fibonacci(2) -> 1
34[0.00003219s]fibonacci(3) -> 2
35[0.00006509s]fibonacci(4) -> 3
36[0.00011182s]fibonacci(5) -> 5
37[0.00023007s]fibonacci(6) -> 8
388
可以看出,除了最后一行,其余输出都是clock装饰器生成的。fibonacci(1)调用了8次,fibonacci(2)调用了5次。
下面使用lru_cache()
1from test import clock
2import functools
3
4@functools.lru_cache()
5@clock
6def fibonacci(n):
7 if n < 2:
8 return n
9 return fibonacci(n - 2) + fibonacci(n - 1)
10
11if __name__ == '__main__':
12 print(fibonacci(6))
13结果:
14[0.00000000s]fibonacci(0) -> 0
15[0.00000000s]fibonacci(1) -> 1
16[0.00005698s]fibonacci(2) -> 1
17[0.00000095s]fibonacci(3) -> 2
18[0.00008106s]fibonacci(4) -> 3
19[0.00000095s]fibonacci(5) -> 5
20[0.00010085s]fibonacci(6) -> 8
218
可以看出,n的每个值只调用一次函数
1@functools.lru_cache(maxsize=128,typed=False)
lru_cache还要两个参数可以调用:
maxsize表示抗议存储多少个调用的结果;
typed表示是否把不同参数类型得到的结果分开保存;
首先看一个简单的函数
1import html
2def htmlize(obj):
3 content = html.escape(repr(obj))
4 return '<pre>{}</pre>'.format(content)
html.escape的作用的是把html文件中的特殊字符(&,<,>,",'等)转换为HTML-safe字符。现在想要对这个函数做一个扩展
1·str:把内部的换行符替换为‘<br>\n’,不使用<pre>使用<p>
3·int:以十进制和十六进制显示数字
5·list:输出一个HTML列表,根据各个元素的类型进行格式化
对于这个需求的解决思路一般是用一长串的if/elif/elif来调用专门的函数解决(当判断输入的内容为str的时候调用例如htmlize_str的方法)。这样不便于模块的拓展,时间一长,htmlize会变得很大,而且与各个专门函数之间的耦合也很紧密。
Python3.4新增的functools.singledispatch装饰器可以把整体方案拆分成多个模块。使用它装饰的普通函数会变成泛函数:根据第一个参数的类型,以不同方式执行相同操作的一组函数。
1import html
2import numbers
3from collections import abc
4from functools import singledispatch
5
6@singledispatch
7def htmlize(obj):
8 content = html.escape(repr(obj))
9 return '<pre>{}</pre>'.format(content)
10
11@htmlize.register(str)
12def _(text):
13 content = html.escape(text).replace('\n', '<br>\n')
14 return '<p>{}</p>'.format(content)
15
16
17@htmlize.register(numbers.Integral)
18def _(n):
19 return '<pre>{0} (0x{0:x})</pre>'.format(n)
20
21@htmlize.register(tuple)
22@htmlize.register(abc.MutableSequence)
23def _(seq):
24 inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
25 return '<ul>\n<li>' + inner + '</li>\n</ul>'
26
27if __name__ == '__main__':
28 print(htmlize({1, 2, 3}))
29 print(htmlize(abs))
30 print(htmlize('Heimlich & Co.\n- a game'))
31 print(htmlize(42))
32 print(htmlize(['alpha', 66, {3, 2, 1}]))
33结果:
34<pre>{1, 2, 3}</pre>
35<pre><built-in function abs></pre>
36<p>Heimlich & Co.<br>
37- a game</p>
38<pre>42 (0x2a)</pre>
39<ul>
40<li><p>alpha</p></li>
41<li><pre>66 (0x42)</pre></li>
42<li><pre>{1, 2, 3}</pre></li>
43</ul>
@singledispatch标记处理object类型的基函数
各个专门函数使用@《base_function》.register(《type》)装饰
由于专门函数的名称没有意义,所以用下划线_表示
number.Integral是int的虚拟超类,和abc.MutableSequence一样都是抽象基类
最后一个函数表明可以叠放多个register装饰器,让同一个函数支持不同类型
在一个类中为同一个方法定义多个重载变体(def a ,def b,def c),比在一个函数里面使用一长串if/elif/elif块要好。@singledispath的优点是支持模块化扩展,各个模块可以为它支持的各个类型注册一个专门的函数。
装饰器是函数,所以可以组合起来使用。(在被装饰的函数上应用装饰器)
@d1
@d2
def f():
print('f')
上面和下面两者是一样的
1def f():
2
3 print('f')
4
5f = d1(d2(f))
Python把装饰的函数作为第一个参数传递给装饰器函数,如果需要让装饰器接受其他的参数的话,需要创建一个装饰器工厂函数,把参数传递给它,返回一个装饰器,然后再把它应用到要装饰的函数上。将第三章的例子改写一下:
1registry = set()
2
3def register(active=True):
4 def decorate(func):
5 print('running register(active=%s)->decorate(%s)' % (active, func))
6 if active:
7 registry.add(func)
8 else:
9 registry.discard(func)
10 return func
11 return decorate
12
13@register(active=False)
14def f1():
15 print('running f1()')
16
17@register()
18def f2():
19 print('running f2()')
20
21def f3():
22 print('running f3()')
与之前的例子进行对比可以发现decorate这个内部函数是真正的装饰器,它的参数是一个函数,它是一个装饰器,所以必须返回一个函数
register是装饰器工厂函数,因此返回decorate
@register工厂函数必须作为函数调用,并且传入所需的参数,如果有默认值那也需要作为函数调用【@register()】,即要返回真正的装饰器decorate
这个例子的关键是,register()要返回decorate,然后把它应用到被装饰的函数上。
1if __name__ == '__main__':
2 print(registry)
图片 3.png
从结果可以看到只有f2加入到了集合中。
装饰器其实就是函数的调用,所以如果不使用@的话
1register()(f)
2register(active=False)(f)
修改一下第五章中clock装饰器,给它添加一个功能:让用户输入一个格式化字符串,控制被装饰函数的输出
1import time
2DEFAULT_FMT = '[{elapsed:0.8f}s]{name}({args} -> {result})'
3
4
5def clock(fmt=DEFAULT_FMT):
6 def decorate(func):
7 def clocked(*_args):
8 t0 = time.time()
9 _result = func(*_args)
10 elapsed = time.time()-t0
11 name = func.__name__
12 args = ','.join(repr(arg) for arg in _args)
13 result = repr(_result)
14 print(fmt.format(**locals()))
15 return _result
16 return clocked
17 return decorate
将之前的格式化输出当初默认的参数输入。
clock是参数化装饰器的工厂函数。decorate是真正的装饰器,clocked包装被装饰的函数。
**locals()是为了在fmt中引用clocked的局部变量。
图片 4.png
1if __name__ == '__main__':
2 @clock()
3 def snooze(secondes):
4 time.sleep(secondes)
5 for i in range(3):
6 snooze(.123)
图片 5.png
下面修改下格式化输出的内容:
1if __name__ == '__main__':
2 @clock('{name}:{elapsed}s')
3 def snooze(secondes):
4 time.sleep(secondes)
5 for i in range(3):
6 snooze(.123)
图片 6.png
1if __name__ == '__main__':
2
3 @clock('{name}({args}) dt={elapsed:0.3f}s')
4
5 def snooze(secondes):
6
7 time.sleep(secondes)
8
9 for i in range(3):
10
11 snooze(.123)
图片 7.png
只要修改的格式化输出的内容包含在clocked的局部变量就可以正常输出了。