python-装饰器
Python 函数装饰器和闭包
函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。这是一项强大的功能,但是若想掌握,必须理解闭包。
修饰器和闭包经常在一起讨论, 因为修饰器就是闭包的一种形式. 闭包还是回调式异步编程和函数式编程风格的基础.装饰器是语法糖, 它其实是将函数作为参数让其他函数处理. 装饰器有两大特征:
- 把被装饰的函数替换成其他函数
- 装饰器在加载模块时立即执行
一、装饰器基础知识
装饰器是可调用的对象, 其参数是另一个函数(被装饰的函数). 装饰器可能会处理被装饰的函数, 然后把它返回, 或者将其替换成另一个函数或可调用对象.
1 |
|
这种写法与下面写法完全等价:
1 | def target(): |
要理解立即执行看下等价的代码就知道了,
target = decorate(target)
这句调用了函数.
一般情况下装饰函数都会将某个函数作为返回值.
二、变量作用域规则
要理解装饰器中变量的作用域, 应该要理解闭包,
我觉得书里将闭包和作用域的顺序换一下比较好. 在python中,
一个变量的查找顺序是 LEGB
(L:Local 局部环境,E:Enclosing
闭包,G:Global 全局,B:Built-in 内置).
1 | base = 20 # 3 |
在闭包的函数 real_compare
中, 使用的变量
base
其实是 base = 10
的.
因为base这个变量在闭包中就能命中, 而不需要去 global
中获取.
三、闭包
闭包其实挺好理解的, 当匿名函数出现的时候, 才使得这部分难以掌握. 简单简短的解释闭包就是:
名字空间与函数捆绑后的结果被称为一个闭包(closure).
这个名字空间就是 LEGB
中的 E
.
所以闭包不仅仅是将函数作为返回值.
而是将名字空间和函数捆绑后作为返回值的. 多少人忘了理解这个
"捆绑"
, 不知道变量最终取的哪和哪啊. 哎.
标准库中的装饰器
python内置了三个用于装饰方法的函数: property
、
classmethod
和 staticmethod
.
这些是用来丰富类的.
1 | class A(object): |
四、应用
非常适合有切面需求的场景,比如权限校验,日志记录和性能测试等等。比如你想要执行某个函数前记录日志或者记录时间来统计性能,又不想改动这个函数,就可以通过装饰器来实现。
不用装饰器,我们会这样来实现在函数执行前插入日志:
1 | def foo(): |
虽然这样写是满足了需求,但是改动了原有的代码,如果有其他的函数也需要插入日志的话,就需要改写所有的函数,不能复用代码。可以这么写:
1 | def use_logg(func): |
这样写的确可以复用插入的日志,缺点就是显示的封装原来的函数,我们希望透明的做这件事。用装饰器来写:
1 | def use_log(func): |
use_log()
就是装饰器,它把真正我们想要执行的函数
bar()
封装在里面,返回一个封装了加入代码的新函数,看起来就像是
bar()
被装饰了一样。这个例子中的切面就是函数进入的时候,在这个时候,我们插入了一句记录日志的代码。这样写还是不够透明,通过@语法糖来起到
bar = use_log(bar)
的作用。
1 | bar = use_log(bar)def use_log(func): |
装饰器也是可以带参数的,这位装饰器提供了更大的灵活性。
1 | def use_log(level): |
实际上是对装饰器的一个函数封装,并返回一个装饰器。这里涉及到作用域的概念,之前有一篇博客提到过。可以把它看成一个带参数的闭包。当使用
@use_log(level='warn')
时,会将 level
的值传给装饰器的环境中。它的效果相当于
use_log(level='warn')(foo)
,也就是一个三层的调用。
这里有一个美中不足,decorator
不会改变装饰的函数的功能,但会悄悄的改变一个 __name__
的属性(还有其他一些元信息),因为 __name__
是跟着函数命名走的。 可以用
@functools.wraps(func)
来让装饰器仍然使用 func
的名字。functools.wraps
也是一个装饰器,它将原函数的元信息拷贝到装饰器环境中,从而不会被所替换的新函数覆盖掉。
1 | import functools |