目前编程很少涉及到装饰器的编写或者使用,简单做一个笔记,方便日后回顾。

  • 装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。
  • 但是要想使用或者深入理解装饰器的工作逻辑,必须理解闭包这一概念。

1. 装饰器在被装饰的函数定义之后立即运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
registry = []

def register(func):
print("running register (%s) (%s)" % (func.__name__, func))
registry.append(func)
return func

@register
def f1():
print("running f1()")

@register
def f2():
print("running f2()")

def f3():
print("running f3()")

def main():
print("running main()")
for f in registry:
print(f.__name__, end=' ')
print('\r')
f1()
f2()
f3()

if __name__ == "__main__":
main()

所以上述代码,在main方法执行之前,装饰器装饰函数时就已经执行了。

如果将上述代码作为模块进行导入,函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。

2. 一个经典的错例子

Python 编译函数的定义体时,它判断 b 是局部变量,因为在函数中给它赋值了。生成的字节码证实了这种判断,Python 会尝试从本地环境获取 b。后面调用 f2(3) 时, f2 的定义体会获取并打印局部变量 a 的值,但是尝试获取局部变量 b 的值时,发现 b 没有 绑定值。

将b正确作为全局变量的使用方法应该如下

1
2
3
4
5
6
b = 5
def f2(a):
global b
print(a)
print(b)
b = 9

3. 闭包

上述问题引出了一个关于变量作用域的问题,而闭包的概念也与变量的作用域有关。

在博客圈,人们有时会把闭包和匿名函数弄混。这是有历史原因的:在函数内部定义函数 不常见,直到开始使用匿名函数才会这样做。而且,只有涉及嵌套函数时才有闭包问题。 因此,很多人是同时知道这两个概念的

闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。

看下面这个例子,很好的帮助我们理解闭包这一概念:

首先是如下make_averager()函数,其中自由变量seriesaverager()也可以访问。根据下面代码可以发现自由变量series实际上是保存在了averager对象的__closure__属性中,在__code__.co_freevars有保存其变量名。

  • 提示:在Python中,函数也是一等对象
  • 一等对象具有如下特性
    • 在运行时创建
    • 能赋值给变量或者数据结构中的元素
    • 能做为参数传递给函数
    • 能做为函数的返回结果
1
2
3
4
5
6
7
8
def make_averager():
series = []
def averager(v):
# 这里仅仅调用了series的append方法,并没有对其赋值
# 如果对其赋值,则averager会将其视为局部变量
series.append()
return sum(series) / len(series)
return averager

下述例子,关于nonlocal关键字:

1
2
3
4
5
6
7
8
9
10
def make_averager():
count = 0
total = 0
def averager(v):
# nonlocal 声明
nonlocal count, total
count += 1
total += v
return total / count
return averager

最后简单写一个计时的装饰器,同时记录递归深度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import functools
import time

DEFAULT_FMT = '[deep: {deep:d} time: {elapsed:0.8f}s] {name} ({args}) -> {result}'

def clock(fmt=DEFAULT_FMT):
def decorate(func):
deep = 0
@functools.wraps(func)
def clocked(*_args):
nonlocal deep
deep += 1
t0 = time.time()
_result = func(*_args)
elapsed = time.time() - t0
deep -= 1
name = func.__name__
args = ','.join(repr(arg) for arg in _args)
result = repr(_result)
print(fmt.format(**locals()))
return _result
return clocked
return decorate

@clock()
def gcd(a:int, b:int):
return gcd(b, a % b) if b else a

if __name__ == "__main__":
a = 256
b = 196
gcd(a, b)