![自学Python:编程基础、科学计算及数据分析](https://wfqqreader-1252317822.image.myqcloud.com/cover/254/34659254/b_34659254.jpg)
3.3 装饰器
3.3.1 装饰器的引入
在Python中,函数本身就是一个对象:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/123_01.jpg?sign=1738869154-dXoz8GbdWey0bmEfUZrziWNStilFTQKD-0-0627d18294cdcac7e03f89591008ce20)
作为对象,函数有一些自己的方法和属性,这些方法和属性可以用dir()函数查看:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/123_02.jpg?sign=1738869154-SZOIgtuDxBS27XvrI9ZjGrMnGhcnVVQH-0-50e2616426cdb47c5ea1ac1be3d0345b)
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/124_01.jpg?sign=1738869154-MLTFY91f2HyNeZUqyXZz7W1WOjjMEvcU-0-88be4d9ec2084792de5916f3ced19da5)
其中.__call__()方法是最重要的一个方法,调用函数foo(x)相当于调用了对象foo的.__call__()方法:
In [4]: foo.__call__(42)
42
函数本身可以作为一个参数传给另一个函数:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/124_02.jpg?sign=1738869154-SFcA0ZifX6MeiFwHDaK0VwR87xqchJiA-0-2dc2f850c5233a4b781a518fe8761b9d)
在介绍装饰器之前,先假设我们定义了这样一个函数add():
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/124_03.jpg?sign=1738869154-RQbc9VGLLWnL8ZkE6kEXBgNskaTsLOBb-0-672e62a1916f8a4b765293af26a897e1)
现在我们希望在调用函数的时候,打印一条相关信息,说明哪个函数被调用了。
最简单的做法是在函数中直接加上一条print语句:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/124_04.jpg?sign=1738869154-lMXKMB3aSUEDoKsvAxuHrl1xOGuNc3OK-0-2782afaab0ac652b5aad901c28907b8f)
不过,除了add()函数,其他的函数都有这样的需求,在每个函数里都加上一行print语句显得比较麻烦。
因为功能相似,我们考虑使用一个公共函数,接受一个函数作为参数,并打印出这个函数相关的信息,最后返回这个函数本身。
函数的名字可以通过函数的.__name__属性获得:
In [8]: add.__name__
Out[8]: 'add'
利用.__name__属性,我们的公共函数定义如下:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/124_05.jpg?sign=1738869154-CuqUJSWyXLQoKcFIfjql7XPK64Mnlu7e-0-3819e190c0b68ac0c6dfce13c200744a)
调用时,可以用loud(add)(1, 2)代替add(1, 2):
In [10]: loud(add)(1, 2)
calling function add
Out[10]: 3
换一个系统自带函数作为参数,比如len()函数:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/125_01.jpg?sign=1738869154-P8JnEBQTHotfTug4gEtPJCUW8op2Gyu8-0-347e7fc56d6306258cc1bf1bbc3a370c)
不过这样的定义方式其实并不完全符合我们的要求。
我们希望在调用函数add()时打印相关信息,但现在的信息是在调用loud(add)的时候打印出来的:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/125_02.jpg?sign=1738869154-Fey7AJfM5rgimWkGlQjflCnGNtlNTBsE-0-a594735784749b20ce017f890bf54da3)
函数add()并没有被调用(没有接受参数),但信息还是被显示了。
为了完成我们想要的功能,可以利用高阶函数的特性,在函数中定义新函数,并将打印信息的功能放到一个内部函数中:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/125_03.jpg?sign=1738869154-JvUMq1omu5KCtE1WvnWnxeXPd1SSBzP0-0-4555d31bb203dba4624829b039bc42bf)
如果我们只是调用loud_info(add)而不传入参数,并不会打印相关信息:
In [14]: loud_info(add)
Out[14]: <function __main__.g>
传入参数时,打印信息:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/125_04.jpg?sign=1738869154-mK0T7DIyBLXAilWvlCUMIAJ0nbKHwFY7-0-93e863267d226b2c2b05700e4689eae8)
在Python中,像loud_info这种为函数添加新特性的函数,一般称为装饰器(Decorator)。
在实际应用中,把函数f的每个调用都改成loud_info(f)显得不是很方面。为此,Python提供了“@”符号来简化装饰器的使用。
我们只需要在add()函数的定义前,加上一个@loud_info标志:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/125_05.jpg?sign=1738869154-NSP1GPH6kJ3xOGqatcjsVVWnib1uPu2B-0-9794069e51d0856ca5d490c18f0debc1)
再调用add()函数,我们会发现装饰器的特性已经被自动加入了:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/126_01.jpg?sign=1738869154-aqnGM2fCmciji2x8WCXB2TGmTr9XBspJ-0-ac2515d5a774405b0ee33257ace73f9c)
3.3.2 装饰器的用法
1. 装饰器的原理
装饰器的本质是一个接受函数参数的函数。
我们定义好一个装饰器A,再用到函数f的定义上:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/126_02.jpg?sign=1738869154-EqJexMLxzeSImEd2Ecy3XWwDaef2zeGd-0-b4139c7bd136c4ec8223fc5f5696a25f)
这相当于进行了一个f=A(f)的操作。
更一般地,使用多个装饰器:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/126_03.jpg?sign=1738869154-TrXW4kbj4fQx2b7EhhdK4OUKoA5ba40t-0-04bf937f737e48a946a983c7d173cb6d)
这相当于进行了一个f=A(B(C(f)))的操作。
@操作符必须一行一个,类似“@A@B”或者“@A def f(): ...”这样的定义都是非法的。
2. 装饰器的实例
我们定义一个名为deco的装饰器,其作用是给函数加一个.attr属性并返回函数本身:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/126_04.jpg?sign=1738869154-1Y0lYBdE4aK7xYXnoKMopxX6uxzrWFEU-0-6562dcb1cc3212fa0950b53a76b884d6)
定义一个函数f,并用deco装饰,其中,pass关键字表示该函数什么都不做:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/126_05.jpg?sign=1738869154-QxqjUxc61UBvoWx9cZT1bZGlgzdgMuZ0-0-cee0f4ebcd3e9c62876a41d06cce2f61)
定义好的函数f有一个.attr的属性:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/126_06.jpg?sign=1738869154-IiPskqhP2PU6aUzVDal2iNsUrCfZfia7-0-c6c700da36261ea99daa355cc88c048b)
同一个装饰器可以作用在多个函数上。
例如,定义一个判断函数参数是否为整数的装饰器:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/127_01.jpg?sign=1738869154-OcHxFKj0vNUYMDLt7PkensYPbMWHQT4R-0-5dc114cbc1c8e471aae8989b39f7f184)
在装饰器函数中我们使用了关键字assert:
assert isinstance(arg, int)
关键字assert通常用来检测之后表达式是否为真,如果表达式值为假,assert会抛出一个异常,中断程序。
isinstance()函数用来检查前一个参数arg是否为后一个参数(通常是类型)的一个实例,如果是,返回True,否则返回False。
将装饰器作用在函数p1和p2上:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/127_02.jpg?sign=1738869154-WvdmDq2iXap9z32NWMhSbzeA3AcMfknW-0-0f66b6030967f21c76469cb101e09f0b)
这样这两个函数都有了判断参数是否为整数的特性。
多个装饰器可以连续作用在同一个函数上。
例如,先定义两个装饰器,第一个装饰器的作用是将函数返回值加1:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/127_03.jpg?sign=1738869154-Ks8SJwxjp5G9TRgtKmacl9sZxbROXCVI-0-ddffd155556aef61f0099ad580d4867c)
第二个的作用是将函数返回值乘以2:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/127_04.jpg?sign=1738869154-MBtodgAvbZkp5I0n5Oy22vOJLUlRk8rP-0-6caf48bbb334f12a4448bef475fff6bc)
定义一个返回本身的函数foo,再加上这两个装饰器:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/128_01.jpg?sign=1738869154-d3mPjmfedt7I3Z6YHpDfVDFAJV3uCXFv-0-dfcf57ff8cd0b3b3f9f6031886dd7ebe)
通过装饰器的作用,现在的foo(x)函数返回的结果为2x+1:
In [11]: foo(13)
Out[11]: 27
3. 装饰器工厂
装饰器还支持这样的用法:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/128_02.jpg?sign=1738869154-JGNuk3ACeJctahHVkUvnJJ4hRLRw87HB-0-381da73993cf6e4517aa7acf7f8a05b8)
这种用法相当于:
D = C(args)
f=A(B(D(f)))
即将C(args)的返回值看成一个新的装饰器D。
我们通过给函数C传入不同的参数,可以生成不同的装饰器函数,因此有人将函数C称为装饰器工厂(Decorator factory)。
在之前的例子中,我们定义了plus_one和times_two两个装饰器,现在我们可以将它们一般化为装饰器工厂。首先将plus_one一般化为一个名为plus_n的装饰器工厂:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/128_03.jpg?sign=1738869154-ncyxCq8Ap5hpHOMKIOPb5H5bZ0kAL8H8-0-1789e42dc5e33aa14bbc54f05507464b)
plus_n()函数接受一个参数n,返回一个装饰器函数,该装饰器函数接受一个函数作为参数,并让函数的返回值加n。
在这个定义下,装饰器plus_one相当于plus_n(1)。
同样的道理,我们将times_two一般化为一个名为times_n的装饰器工厂:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/128_04.jpg?sign=1738869154-micV8mIwC5CnkrNgqql4365UNNFVDLfn-0-e93df93d6058bbd8dca0a7bef8f4fb1b)
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/129_01.jpg?sign=1738869154-bHlZhVZ912LRoKWw3zbAErvaO2Ebdu0T-0-4317d13a517b4e6a41fde816bbfa069b)
times_n()函数接受一个参数n,返回一个实现返回值乘n的装饰器函数。
在这个定义下,装饰器times_two相当于times_n(2)。
我们可以这样重新定义foo:
![](https://epubservercos.yuewen.com/9B8D30/18513172808564606/epubprivate/OEBPS/Images/129_02.jpg?sign=1738869154-z9cILEZOzI2bBkhIQ99JWiBTnm2DQmyi-0-8093a023ff61851cbe46556193c6c407)