python-装饰器

Python 函数装饰器和闭包

函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。这是一项强大的功能,但是若想掌握,必须理解闭包。

修饰器和闭包经常在一起讨论, 因为修饰器就是闭包的一种形式. 闭包还是回调式异步编程函数式编程风格的基础.装饰器是语法糖, 它其实是将函数作为参数让其他函数处理. 装饰器有两大特征:

  • 把被装饰的函数替换成其他函数
  • 装饰器在加载模块时立即执行

一、装饰器基础知识

装饰器可调用的对象, 其参数是另一个函数(被装饰的函数). 装饰器可能会处理被装饰的函数, 然后把它返回, 或者将其替换成另一个函数或可调用对象.

1
2
3
@decorate
def target():
print('running target()')复制代码

这种写法与下面写法完全等价:

1
2
3
def target():
print('running target()')
target = decorate(target)

要理解立即执行看下等价的代码就知道了, target = decorate(target) 这句调用了函数. 一般情况下装饰函数都会将某个函数作为返回值.

二、变量作用域规则

要理解装饰器中变量的作用域, 应该要理解闭包, 我觉得书里将闭包和作用域的顺序换一下比较好. 在python中, 一个变量的查找顺序是 LEGB (L:Local 局部环境,E:Enclosing 闭包,G:Global 全局,B:Built-in 内置).

1
2
3
4
5
6
7
8
9
10
base = 20 # 3
def get_compare():
base = 10 # 2
def real_compare(value):
base = 5 # 1
return value > base
return real_compare

compare_10 = get_compare()
print(compare_10(5))复制代码

在闭包的函数 real_compare 中, 使用的变量 base 其实是 base = 10 的. 因为base这个变量在闭包中就能命中, 而不需要去 global 中获取.

三、闭包

闭包其实挺好理解的, 当匿名函数出现的时候, 才使得这部分难以掌握. 简单简短的解释闭包就是:

名字空间与函数捆绑后的结果被称为一个闭包(closure).

这个名字空间就是 LEGB 中的 E . 所以闭包不仅仅是将函数作为返回值. 而是将名字空间和函数捆绑后作为返回值的. 多少人忘了理解这个 "捆绑" , 不知道变量最终取的哪和哪啊. 哎.

标准库中的装饰器

python内置了三个用于装饰方法的函数: propertyclassmethodstaticmethod . 这些是用来丰富类的.

1
2
3
4
class A(object):
@property
def age():
return 12

四、应用

非常适合有切面需求的场景,比如权限校验,日志记录和性能测试等等。比如你想要执行某个函数前记录日志或者记录时间来统计性能,又不想改动这个函数,就可以通过装饰器来实现。

不用装饰器,我们会这样来实现在函数执行前插入日志:
1
2
3
4
5
def foo():
print('i am foo')
def foo():
print('foo is running')
print('i am foo')

虽然这样写是满足了需求,但是改动了原有的代码,如果有其他的函数也需要插入日志的话,就需要改写所有的函数,不能复用代码。可以这么写

1
2
3
4
5
6
def use_logg(func):
logging.warn("%s is running" % func.__name__)
func()
def bar():
print('i am bar')
use_log(bar) #将函数作为参数传入

这样写的确可以复用插入的日志,缺点就是显示的封装原来的函数,我们希望透明的做这件事。用装饰器来写:

1
2
3
4
5
6
7
8
9
10
11
def use_log(func):
def wrapper(*args,**kwargs):
logging.warn('%s is running' % func.__name___)
return func(*args,**kwargs)
return wrapper

def bar():
print('I am bar')

bar = use_log(bar)
bar()

use_log() 就是装饰器,它把真正我们想要执行的函数 bar() 封装在里面,返回一个封装了加入代码的新函数,看起来就像是 bar() 被装饰了一样。这个例子中的切面就是函数进入的时候,在这个时候,我们插入了一句记录日志的代码。这样写还是不够透明,通过@语法糖来起到 bar = use_log(bar) 的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bar = use_log(bar)def use_log(func):
def wrapper(*args,**kwargs):
logging.warn('%s is running' % func.__name___)
return func(*args,**kwargs)
return wrapper

@use_log
def bar():
print('I am bar')

@use_log
def haha():
print('I am haha')

bar()
haha()
装饰器也是可以带参数的,这位装饰器提供了更大的灵活性。
1
2
3
4
5
6
7
8
9
10
11
12
13
def use_log(level):
def decorator(func):
def wrapper(*args, **kwargs):
if level == "warn":
logging.warn("%s is running" % func.__name__)
return func(*args)
return wrapper
return decorator

@use_log(level="warn")
def foo(name='foo'):
print("i am %s" % name)
foo()

实际上是对装饰器的一个函数封装,并返回一个装饰器。这里涉及到作用域的概念,之前有一篇博客提到过。可以把它看成一个带参数的闭包。当使用 @use_log(level='warn') 时,会将 level 的值传给装饰器的环境中。它的效果相当于 use_log(level='warn')(foo) ,也就是一个三层的调用。

这里有一个美中不足,decorator 不会改变装饰的函数的功能,但会悄悄的改变一个 __name__ 的属性(还有其他一些元信息),因为 __name__ 是跟着函数命名走的。 可以用 @functools.wraps(func) 来让装饰器仍然使用 func 的名字。functools.wraps 也是一个装饰器,它将原函数的元信息拷贝到装饰器环境中,从而不会被所替换的新函数覆盖掉。

1
2
3
4
5
6
7
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper