3.3 装饰器
3.3.1 装饰器的引入
在Python中,函数本身就是一个对象:
作为对象,函数有一些自己的方法和属性,这些方法和属性可以用dir()函数查看:
其中.__call__()方法是最重要的一个方法,调用函数foo(x)相当于调用了对象foo的.__call__()方法:
In [4]: foo.__call__(42)
42
函数本身可以作为一个参数传给另一个函数:
在介绍装饰器之前,先假设我们定义了这样一个函数add():
现在我们希望在调用函数的时候,打印一条相关信息,说明哪个函数被调用了。
最简单的做法是在函数中直接加上一条print语句:
不过,除了add()函数,其他的函数都有这样的需求,在每个函数里都加上一行print语句显得比较麻烦。
因为功能相似,我们考虑使用一个公共函数,接受一个函数作为参数,并打印出这个函数相关的信息,最后返回这个函数本身。
函数的名字可以通过函数的.__name__属性获得:
In [8]: add.__name__
Out[8]: 'add'
利用.__name__属性,我们的公共函数定义如下:
调用时,可以用loud(add)(1, 2)代替add(1, 2):
In [10]: loud(add)(1, 2)
calling function add
Out[10]: 3
换一个系统自带函数作为参数,比如len()函数:
不过这样的定义方式其实并不完全符合我们的要求。
我们希望在调用函数add()时打印相关信息,但现在的信息是在调用loud(add)的时候打印出来的:
函数add()并没有被调用(没有接受参数),但信息还是被显示了。
为了完成我们想要的功能,可以利用高阶函数的特性,在函数中定义新函数,并将打印信息的功能放到一个内部函数中:
如果我们只是调用loud_info(add)而不传入参数,并不会打印相关信息:
In [14]: loud_info(add)
Out[14]: <function __main__.g>
传入参数时,打印信息:
在Python中,像loud_info这种为函数添加新特性的函数,一般称为装饰器(Decorator)。
在实际应用中,把函数f的每个调用都改成loud_info(f)显得不是很方面。为此,Python提供了“@”符号来简化装饰器的使用。
我们只需要在add()函数的定义前,加上一个@loud_info标志:
再调用add()函数,我们会发现装饰器的特性已经被自动加入了:
3.3.2 装饰器的用法
1. 装饰器的原理
装饰器的本质是一个接受函数参数的函数。
我们定义好一个装饰器A,再用到函数f的定义上:
这相当于进行了一个f=A(f)的操作。
更一般地,使用多个装饰器:
这相当于进行了一个f=A(B(C(f)))的操作。
@操作符必须一行一个,类似“@A@B”或者“@A def f(): ...”这样的定义都是非法的。
2. 装饰器的实例
我们定义一个名为deco的装饰器,其作用是给函数加一个.attr属性并返回函数本身:
定义一个函数f,并用deco装饰,其中,pass关键字表示该函数什么都不做:
定义好的函数f有一个.attr的属性:
同一个装饰器可以作用在多个函数上。
例如,定义一个判断函数参数是否为整数的装饰器:
在装饰器函数中我们使用了关键字assert:
assert isinstance(arg, int)
关键字assert通常用来检测之后表达式是否为真,如果表达式值为假,assert会抛出一个异常,中断程序。
isinstance()函数用来检查前一个参数arg是否为后一个参数(通常是类型)的一个实例,如果是,返回True,否则返回False。
将装饰器作用在函数p1和p2上:
这样这两个函数都有了判断参数是否为整数的特性。
多个装饰器可以连续作用在同一个函数上。
例如,先定义两个装饰器,第一个装饰器的作用是将函数返回值加1:
第二个的作用是将函数返回值乘以2:
定义一个返回本身的函数foo,再加上这两个装饰器:
通过装饰器的作用,现在的foo(x)函数返回的结果为2x+1:
In [11]: foo(13)
Out[11]: 27
3. 装饰器工厂
装饰器还支持这样的用法:
这种用法相当于:
D = C(args)
f=A(B(D(f)))
即将C(args)的返回值看成一个新的装饰器D。
我们通过给函数C传入不同的参数,可以生成不同的装饰器函数,因此有人将函数C称为装饰器工厂(Decorator factory)。
在之前的例子中,我们定义了plus_one和times_two两个装饰器,现在我们可以将它们一般化为装饰器工厂。首先将plus_one一般化为一个名为plus_n的装饰器工厂:
plus_n()函数接受一个参数n,返回一个装饰器函数,该装饰器函数接受一个函数作为参数,并让函数的返回值加n。
在这个定义下,装饰器plus_one相当于plus_n(1)。
同样的道理,我们将times_two一般化为一个名为times_n的装饰器工厂:
times_n()函数接受一个参数n,返回一个实现返回值乘n的装饰器函数。
在这个定义下,装饰器times_two相当于times_n(2)。
我们可以这样重新定义foo: