自学Python:编程基础、科学计算及数据分析
上QQ阅读APP看书,第一时间看更新

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: