装饰器是 python 的特色语法之一,它可以用来做面向切面编程(AOP)。 相比其它语言,python 的装饰器语法要简洁的多,因为它充分利用了高阶函数这一强大的特性。
但是在具体的使用上,总会遇到各种各样的语法上细枝末节的纠缠。 所以今天我总结下有关 python 装饰器的语法细节,也算是给自己的一个参考吧。
1. 基本的装饰器
1.1. 使用
一个普通的装饰器的使用类似于这样:
|
|
假设这个 log 装饰器是添加日志功能,那么每次 add 函数的调用,都会自动的记录下日志信息。
在函数定义前用@符号加上装饰器,就会给人一种感觉,好像这个函数被@这种强大的魔法装饰了一样。 这里,由于被@的强大魔法所影响,这个函数被增加了日志记录的功能。
但是实际上,这种@的语法只不过是一种语法糖而已。上面的代码完全等价于:
|
|
在 python 中,一切变量都是引用类型,而且函数又是 first-class 的。 所以定义 add 函数,其实是创建了一个函数,并且将 add 作为它的引用。 如果我愿意,我完全可以让 add 重新引用到另外一个函数上。
可以看到,python 装饰器语法的实质,其实是将 add 函数传入到 log 函数,将返回的函数重新引用到 add 上。
那么这个 log,本质上也是个函数,是高阶函数,它接收一个被装饰的函数作为参数,然后返回装饰后的函数。
1.2. 定义
知道了装饰器是个传入被装饰函数返回装饰后函数的高阶函数,那么定义装饰器也就很简单了。
|
|
Call function add
log 函数里面定义的 wrapper 函数,这个函数会调用原本的被装饰函数(这个例子里是 add 函数),并且在此基础上做一些额外的工作。
然后 log 函数返回 wrapper 函数,这个函数会替代原本的 add 函数绑定到 add 这个名称上,变成了新的 add 函数。
之后调用 add 函数,其实是调用被调包过的 wrapper 函数。这样,使用@语法轻轻一装饰,函数的功能就被增强了。
1.3. 函数签名问题
实际上,在用 def 定义函数的时候,不仅仅是像上面所说的,创建一个函数,然后把 add 这个名字引用到这个函数上。
用 def 定义 add 函数时还会把这个函数的 __name__
属性设为”add”。除此之外,这个函数还有 docstring, 参数列表等元信息。
但是如果使用上面的装饰器定义,最终 wrapper 函数会替换原本的 add 函数,那么原本的这些元信息就丢失了。来看看:
|
|
wrapper
None
解决方法也很明显,把原本函数的元信息 copy 到包装函数 wrapper,就行了。 python 的标准库 functools 里提供了一个装饰器 wraps,你只需要拿它装饰下包装函数,它就帮定搞定这些事情。
那么就有:
|
|
add
返回 a 和 b 的和
2. 带参数的装饰器
假设我想在使用装饰器的时候,传给它一些参数以更好的控制其行为,比如这样:
|
|
同样,由于@语法只是一层语法糖,它等同于:
|
|
很显然,调用 log(“basic”)会返回给你一个上面介绍的基本的装饰器,之后再调用它包装 add 函数。
2.1. 函数写法
有了上面的分析,实现这样的装饰器写起来就很简单了。你需要在 log 函数里面定义基本的装饰器,然后把它返回。
|
|
basic: Call function add
上面的代码中,decorator 函数,其实就是之前定义过的基本的装饰器,log 函数会返回这个基本的装饰器。
在使用 @log("basic")
装饰 add 函数时,会调用 log(“basic”)得到其返回的基本的装饰器,用它来装饰 add 函数。
2.2. 类写法
上面的函数写法,函数里面套函数,整整套了三层,不熟悉函数式编程的人,可能理解起来有点费劲。
python 是一门应用“鸭子类型”的语言,上面提到的各种函数调用,如 add = log("basic")(add)
, 其中 log("basic")
返回的,并不一定非得是个函数。 在 python 中,一切拥有 __call__
方法的对象,都可以当成函数来调用。这种对象称为 可调用对象 (callable)。
这意味着,log 有可能是个类, log("basic")
其实是创建了一个 log 对象,它又是个可调用对象,它的 __call__
方法,其实是个基本的装饰器。
既然这样,就有了下面的另外一种定义带参数装饰器的方式:
|
|
basic: Call function add
2.3. 小结
对于类似 @log("basic")
,其实是传给装饰器一些数据,之后包装函数用这些数据来做额外的工作,比如这里的打印日志。
使用函数写法定义出的带参数装饰器,数据其实是放在闭包中的。而类写法的带参数装饰器,数据是通过构造函数传入保存到了对象的属性中。
如果装饰器的逻辑很复杂,虽然对于第一种写法,使用函数式的技巧也能很好的组织代码。 但是对 python 这种对面向对象编程支持更好的语言来说,还是推荐使用类的写法组织复杂的逻辑。
3. 装饰器装饰类
自 python2.6 以后,装饰器的@语法不仅能对函数使用,还可以对类使用:
|
|
同样,@只是下面等价的语法糖:
|
|
利用装饰类的装饰器,我可以给类添加一些功能,比如添加某些属性和方法:
|
|
实际上,log 函数在给类添加了几个属性后,就把类原封不动的返回了。之后这个被包装的类,就多了 logger 这个属性。
利用和上面相似的方法,同样能够写出带参数的装饰器:
|
|
4. 多装饰器的顺序问题
|
|
这种多个装饰器装饰一个函数的语法,把语法糖的外衣剥开其实是这样的:
|
|
可以看到,越往下的装饰器,它对函数的包装也就越靠里面。
有些时候使用多个装饰器,是有顺序要求的。这个时候记住上面的变换,也就很清楚的知道到底应该把哪个装饰器写在前面的。
5. 最后
python 的装饰器提供了一种很方便的方式来对一个函数或类进行包装。
由于借鉴了函数式的写法,装饰器的语法非常简洁而且统一。
那统一在哪呢?其实装饰器本来用 python 已经有的语法就能轻松实现, 这种各种特性其实背后都有一种统一的规律,而不用规定大量复杂晦涩的特例,本身就能给人一种统一美。
然而 python 使用@语法给这种已经有的写法加了一个小小的语法糖,使其更符合人的直觉,让本来就很好的语法变得更加好用了。
python 类似的简洁优美的特点还有很多,所以用 python 编程,有时候能体会到一种美的享受!