1. 装饰器
函数装饰器用于在源码中"标记"函数,以某种方式增强函数的行为.严格来说装饰器这种形式是一种语法糖.
装饰器的特点有两个:
- 装饰器是可调用的对象,其参数是另一个可调用对象.
- 装饰器可能会处理被装饰的可调用对象,然后把它返回,或者将其替换成另一个可调用对象
- 装饰器在加载模块时立即执行
装饰器的形式如下:
@decorator def call(args): pass
它等价于 call(args) = decorator(call(args))
本章需要的先验知识有:
1.1. 实现装饰器
下面这个例子定义了一个装饰器,用来在每次调用被装饰的函数时计时,然后把经过的时间,传入的参数和调用的结果打印出来.
import time def timer(func): def clocked(*args,**kw): t0 = time.perf_counter() result = func(*args,**kw) elapsed = time.perf_counter() - t0 name = func.__name__ if args: arg = ",".join(repr(arg) for arg in args) if kw: kws = ",".join([repr(i)+"="+repr(v) for i,v in sorted(kw.items())]) arg = arg+","+kws print("[used:{}s] function ".format(elapsed)+func.__name__+"("+arg+")->"+repr(result)) return result return clocked
@timer def snooze(seconds): time.sleep(seconds)
snooze(2)
[used:2.0047967199934646s] function snooze(2)->None
@timer def factorial(n): """factorial""" return 1 if n < 2 else n*factorial(n-1)
factorial(5)
[used:7.310009095817804e-07s] function factorial(1)->1 [used:0.0001588229788467288s] function factorial(2)->2 [used:0.0001920460199471563s] function factorial(3)->6 [used:0.00022201001411303878s] function factorial(4)->24 [used:0.00025161399389617145s] function factorial(5)->120 120
factorial.__name__
'clocked'
1.2. 包装装饰器
上面的装饰器有个缺点--遮盖了被装饰函数的__name__
和__doc__
属性.这时可以使用functools.wraps
装饰器把相关的属性从func
复制到clocked
中
import time def timer(func): def clocked(*args,**kw): t0 = time.perf_counter() result = func(*args,**kw) elapsed = time.perf_counter() - t0 name = func.__name__ if args: arg = ",".join(repr(arg) for arg in args) if kw: kws = ",".join([repr(i)+"="+repr(v) for i,v in sorted(kw.items())]) arg = arg+","+kws print("[used:{}s] function ".format(elapsed)+func.__name__+"("+arg+")->"+repr(result)) return result return clocked
0
@timer def snooze(seconds): time.sleep(seconds)
snooze(2)
import time def timer(func): def clocked(*args,**kw): t0 = time.perf_counter() result = func(*args,**kw) elapsed = time.perf_counter() - t0 name = func.__name__ if args: arg = ",".join(repr(arg) for arg in args) if kw: kws = ",".join([repr(i)+"="+repr(v) for i,v in sorted(kw.items())]) arg = arg+","+kws print("[used:{}s] function ".format(elapsed)+func.__name__+"("+arg+")->"+repr(result)) return result return clocked
3
@timer def factorial(n): """factorial""" return 1 if n < 2 else n*factorial(n-1)
import time def timer(func): def clocked(*args,**kw): t0 = time.perf_counter() result = func(*args,**kw) elapsed = time.perf_counter() - t0 name = func.__name__ if args: arg = ",".join(repr(arg) for arg in args) if kw: kws = ",".join([repr(i)+"="+repr(v) for i,v in sorted(kw.items())]) arg = arg+","+kws print("[used:{}s] function ".format(elapsed)+func.__name__+"("+arg+")->"+repr(result)) return result return clocked
5
import time def timer(func): def clocked(*args,**kw): t0 = time.perf_counter() result = func(*args,**kw) elapsed = time.perf_counter() - t0 name = func.__name__ if args: arg = ",".join(repr(arg) for arg in args) if kw: kws = ",".join([repr(i)+"="+repr(v) for i,v in sorted(kw.items())]) arg = arg+","+kws print("[used:{}s] function ".format(elapsed)+func.__name__+"("+arg+")->"+repr(result)) return result return clocked
6
factorial.__name__
import time def timer(func): def clocked(*args,**kw): t0 = time.perf_counter() result = func(*args,**kw) elapsed = time.perf_counter() - t0 name = func.__name__ if args: arg = ",".join(repr(arg) for arg in args) if kw: kws = ",".join([repr(i)+"="+repr(v) for i,v in sorted(kw.items())]) arg = arg+","+kws print("[used:{}s] function ".format(elapsed)+func.__name__+"("+arg+")->"+repr(result)) return result return clocked
8
1.3. 带参数的装饰器
我们修改之前的timer,希望它可以添加一个参数,用于指定秒数的精确位数.这样就需要写一个带参数的装饰器.
带参数的装饰器我们还需要再在外面套一层用来返回我们的装饰器函数.
import time def timer(func): def clocked(*args,**kw): t0 = time.perf_counter() result = func(*args,**kw) elapsed = time.perf_counter() - t0 name = func.__name__ if args: arg = ",".join(repr(arg) for arg in args) if kw: kws = ",".join([repr(i)+"="+repr(v) for i,v in sorted(kw.items())]) arg = arg+","+kws print("[used:{}s] function ".format(elapsed)+func.__name__+"("+arg+")->"+repr(result)) return result return clocked
9
@timer def snooze(seconds): time.sleep(seconds)
0
@timer def snooze(seconds): time.sleep(seconds)
1
@timer def snooze(seconds): time.sleep(seconds)
2
@timer def snooze(seconds): time.sleep(seconds)
3
@timer def snooze(seconds): time.sleep(seconds)
1
@timer def snooze(seconds): time.sleep(seconds)
5
factorial.__name__
import time def timer(func): def clocked(*args,**kw): t0 = time.perf_counter() result = func(*args,**kw) elapsed = time.perf_counter() - t0 name = func.__name__ if args: arg = ",".join(repr(arg) for arg in args) if kw: kws = ",".join([repr(i)+"="+repr(v) for i,v in sorted(kw.items())]) arg = arg+","+kws print("[used:{}s] function ".format(elapsed)+func.__name__+"("+arg+")->"+repr(result)) return result return clocked
8
@timer def snooze(seconds): time.sleep(seconds)
8
import time def timer(func): def clocked(*args,**kw): t0 = time.perf_counter() result = func(*args,**kw) elapsed = time.perf_counter() - t0 name = func.__name__ if args: arg = ",".join(repr(arg) for arg in args) if kw: kws = ",".join([repr(i)+"="+repr(v) for i,v in sorted(kw.items())]) arg = arg+","+kws print("[used:{}s] function ".format(elapsed)+func.__name__+"("+arg+")->"+repr(result)) return result return clocked
8
当然了返回装饰器函数的对象只要是可执行对象就行.因此或许用类来实现看起来会更加自然一些
snooze(2)
0
@timer def snooze(seconds): time.sleep(seconds)
3
snooze(2)
2
snooze(2)
3
factorial.__name__
import time def timer(func): def clocked(*args,**kw): t0 = time.perf_counter() result = func(*args,**kw) elapsed = time.perf_counter() - t0 name = func.__name__ if args: arg = ",".join(repr(arg) for arg in args) if kw: kws = ",".join([repr(i)+"="+repr(v) for i,v in sorted(kw.items())]) arg = arg+","+kws print("[used:{}s] function ".format(elapsed)+func.__name__+"("+arg+")->"+repr(result)) return result return clocked
8
但从开销角度来看,显然使用函数闭包实现带参数装饰器会比使用带__call__
方法的类实例来的更加好,毕竟函数一旦定义,调用的开销远比实例化一个类小的多,但如果需要实现一些复杂的状态管理功能,这种开销或许也是值得的.
1.4. 类装饰器
装饰器除了可以装饰函数,也可以装饰类,原理也差不多,参数是一个类,而返回的也是一个类,下面以之前的LineItem作为例子讲解如何定义和使用类装饰器.
1.4.1. 定制描述符的类装饰器
在特性与描述符部分的倒数第二个LineItem
例子中储存属性的名称不具有描述性,即属性(如weight)的值存储在名为_Quantity#0
的实例属性中,这样的名称不便于调试的问题.单靠描述符无法存储属性名字,因为实例化描述符时无法得知托管属性(即绑定到描述符上的类属性,例如前述示例中的weight)的名称. 可是,一旦组建好整个类,而且把描述符绑定到类属性上之后,我们就可以审查类,并为描述符设置合理的储存属性名称.LineItem
类的__new__
方法可以做到这一点,因此,在__init__
方法中使用描述符时,储存属性已经设置了正确的名称,
为了解决这个问题而使用__new__
方法纯属白费力气--每次新建LineItem
实例时都会运行__new__
方法中的逻辑,可是,一旦LineItem
类构建好了,描述符与托管属性之间的绑定就不会变了.因此,我们要在创建类时设置储存属性的名称.
使用3.6的新接口__set_name__
,类装饰器或元类都可以做到这一点.这边的例子使用类装饰器
snooze(2)
6
snooze(2)
7
snooze(2)
8
snooze(2)
9
[used:2.0047967199934646s] function snooze(2)->None
0
[used:2.0047967199934646s] function snooze(2)->None
1
[used:2.0047967199934646s] function snooze(2)->None
2
类装饰器能以较简单的方式做到元类做的事情——创建类时定制类.
但类装饰器也有个重大缺点:只对直接依附的类有效.这意味着,被装饰的类的子类可能继承也可能不继承装饰器所做的改动,具体情况视改动的方式而定.
1.5. 标准库中的装饰器
Python内置了三个用于装饰方法的函数:property
、classmethod
和staticmethod
.这三个在面向对象惯用法部分讲.
而剩下的装饰器中
functools.total_ordering
是用来装饰类的functools.lru_cache
,functools.singledispatch
是用来装饰函数/方法的
1.5.1. functools.total_ordering自动添加比较特殊方法
functools.total_ordering
装饰器可以装饰一个类,只要其中有实现__lt__
、__le__
、__gt__
、__ge__
中的至少一个,它就会将其他的补全
[used:2.0047967199934646s] function snooze(2)->None
3
[used:2.0047967199934646s] function snooze(2)->None
4
1.5.2. 使用functools.lru_cache(maxsize=128, typed=False)做备忘
functools.lru_cache
是非常实用的装饰器,它实现了备忘(memoization
)功能.这是一项优化技术,它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算.LRU 三个字母是"Least Recently Used"的缩写,表明缓存不会无限制增长,一段时间不用的缓存条目会被扔掉.
maxsize
参数指定存储多少个调用的结果.缓存满了之后,旧的结果会被扔掉,腾出空间.为了得到最佳性能,maxsize 应该设为2的幂.- typed 参数如果设为True,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如1 和1.0)区分开.
因为lru_cache
使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被lru_cache
装饰的函数,它的所有参数都必须是可散列的.
生成第n个斐波纳契数这种慢速递归函数适合使用lru_cache
[used:2.0047967199934646s] function snooze(2)->None
5
[used:2.0047967199934646s] function snooze(2)->None
6
[used:2.0047967199934646s] function snooze(2)->None
7
浪费时间的地方很明显:fibonacci(1)
调用了8 次,fibonacci(2)
调用了5 次……但是,如果增加两行代码,使用lru_cache,性能会显著改善
[used:2.0047967199934646s] function snooze(2)->None
8
[used:2.0047967199934646s] function snooze(2)->None
9
[used:2.0047967199934646s] function snooze(2)->None
6
@timer def factorial(n): """factorial""" return 1 if n < 2 else n*factorial(n-1)
1
PS:装饰器的叠放顺序也是有讲究的,它是从下向上执行的,因此最终执行的结果是最上面一层的包装.
1.5.3. 使用functools.singledispatch实现单分配泛函
假设我们在开发一个调试Web应用的工具,我们想生成HTML,显示不同类型的Python对象,我们可能会编写这样的函数:
@timer def factorial(n): """factorial""" return 1 if n < 2 else n*factorial(n-1)
2
这个函数适用于任何Python类型,但是现在我们想做个扩展,让它使用特别的方式显示某些类型.
- str:把内部的换行符替换为
\<br\>\n
;不使用\<pre\>
,而是使用\<p\>
。 - int:以十进制和十六进制显示数字。
- list:输出一个HTML列表,根据各个元素的类型进行格式化。
因为Python不支持重载方法或函数,所以我们不能使用不同的签名定义htmlize
的变体,也无法使用不同的方式处理不同的数据类型.在Python中,一种常见的做法是把htmlize
变成一个分派函数,使用一串if/elif/elif
,调用专门的函数,如htmlize_str
,htmlize_int
,等等,这样不便于模块的用户扩展,还显得笨拙:时间一长,分派函数htmlize
会变得很大,而且它与各个专门函数之间的耦合也很紧密.
functools.singledispatch
装饰器可以把整体方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数.使用@singledispatch
装饰的普通函数会变成泛函数(generic function
):根据第一个参数的类型,以不同方式执行相同操作的一组函数.
@timer def factorial(n): """factorial""" return 1 if n < 2 else n*factorial(n-1)
3
@timer def factorial(n): """factorial""" return 1 if n < 2 else n*factorial(n-1)
4
@timer def factorial(n): """factorial""" return 1 if n < 2 else n*factorial(n-1)
5
@timer def factorial(n): """factorial""" return 1 if n < 2 else n*factorial(n-1)
6
@timer def factorial(n): """factorial""" return 1 if n < 2 else n*factorial(n-1)
7
@timer def factorial(n): """factorial""" return 1 if n < 2 else n*factorial(n-1)
8
@timer def factorial(n): """factorial""" return 1 if n < 2 else n*factorial(n-1)
9
只要可能,注册的专门函数应该处理抽象基类(如numbers.Integral
和abc.MutableSequence
),不要处理具体实现(如int 和list).这样,代码支持的兼容类型更广泛.例如,Python扩展可以子类化numbers.Integral
,使用固定的位数实现int
类型.
singledispatch
机制的一个显著特征是,你可以在系统的任何地方和任何模块中注册专门函数.如果后来在新的模块中定义了新的类型,可以轻松地添加一个新的专门函数来处理那个类型.此外,你还可以为不是自己编写的或者不能修改的类添加自定义函数.
singledispatch
是经过深思熟虑之后才添加到标准库中的,它提供的特性很多,这里无法一一说明.这个机制最好的文档是PEP 443 — Single-dispatch generic functions.
@singledispatch
不是为了把Java的那种方法重载带入Python.在一个类中为同一个方法定义多个重载变体,比在一个函数中使用一长串if/elif/elif/elif
块要更好.但是这两种方案都有缺陷,因为它们让代码单元(类或函数)承担的职责太多.@singledispath
的优点是支持模块化扩展:各个模块可以为它支持的各个类型注册一个专门函数.
还没有评论,来说两句吧...