0%

[python源码分析] 装饰器

要了解装饰器,首先我们来看看嵌套函数 ## 嵌套函数

1
2
3
4
def adder(n):
def handler(x):
return n+x
return handler #返回的是一个函数的引用
像 adder 函数和 handler 这样,在一个函数的函数体内定义另一个函数,就构成了 嵌套函数 。adder-handler 这段简单的代码包含了 3 个不同的作用域:

作用域是一个 静态概念,由 Python 代码语法决定,与编译后产生的 代码对象 一一对应。作用域规定了能够被某个代码块访问的变量有哪些,但对变量具体的值则一概不关心。

一旦 Python 程序开始运行,虚拟机需要为 作用域中的变量 分配一定的 存储空间,这就是 名字空间 。名字空间依照作用域规则实现,它 决定了某个变量在运行时的取值,可以看做是 作用域在运行时的动态表现方式。

adder 函数执行时,作用域 A 在虚拟机中表现为 全局 名字空间,作用域 B 表现为 局部 名字空间:

1
2
globals: adder
locals: n, handler
handler 函数执行时,例如调用 adder(10) 时,作用域 A 在虚拟机中表现为 全局 名字空间,作用域 B 表现为 闭包 名字空间:作用域 C 表现为 局部 名字空间
1
2
3
globals: adder
locals: x
enclosure: n

闭包#

什么是闭包呢?#

闭包 ( closure )词法闭包 ( Lexical Closure ) 的简称,指 延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。简单来说就是 嵌套函数引用了外层函数的变量。这些被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。

首先我们来看看adder(10)示例:

1
2
3
4
5
>>> add10 = adder(10)
>>> add10(10)
20
>>> add10(15)
25
观察上面的例子,当函数 adder 返回,局部变量 n 应该就被回收了,为什么 handler 函数还能访问到它呢?这正是由于 闭包空间的存在。

以 adder(10) 为例,它是一个 handler 函数对象,闭包变量 n 值总是 10 。那么,内层函数是如何访问闭包作用域的呢?我们对函数代码对象进行反汇编,从中可以看出端倪:

1
2
3
4
5
6
7
8
9
10
>>> add10 = adder(10)
>>> add10
<function adder.<locals>.handler at 0x10dc2b6a8>
>>> add10.__code__
<code object handler at 0x10dbe5150, file "<stdin>", line 2>
>>> dis.dis(add10.__code__)
3 0 LOAD_DEREF 0 (n) # 执行了闭包变量(藏于 PyFrameObject)查找工作
2 LOAD_FAST 0 (x) # 局部变量(栈帧对象中)查找
4 BINARY_ADD
6 RETURN_VALUE

PyFrameObject 结构体最后部分是不固定的,依次存放着 静态局部名字空间、闭包名字空间以及临时栈。以 add10(1) 为例,函数运行时 PyFrameObject 状态如下如下:

由于函数 局部变量、闭包变量个数编译阶段 就能 确定运行时并不会增减,因此 无须用 dict 对象来保存。相反,将这些变量依次排列 保存在数组中,然后通过数组下标来访问即可。这就是所谓的 静态名字空间

对于局部变量 n ,数组对应的槽位保存着整数对象 1 的地址,表示 n 与 1 绑定。而闭包变量 x 则略有差别,槽位 不直接保存整数对象 10 ,而是通过一个 PyCellObject 间接与整数对象 10 绑定。

闭包变量如何初始化#

函数对象 PyFunctionObject 中有一个字段 func_closure ,保存着函数 所有闭包变量。我们可以通过名字 closure 可以 访问 到这个底层结构体字段:

add10.__closure__ (<cell at 0x10dc09e28: int object at 0x10da161a0>,)

这是一个由 *PyCellObject 组成的 元组,PyCellObject 则 保存着闭包变量的值。当函数调用发生时,Python 虚拟机创建 PyFrameObject 对象,并从 函数对象取出该元组,依次填充相关静态槽位

为什么闭包变量要通过 PyCellObject 间接引用?#

最新的 Python(3.7+) 提供了 nonlocal 关键字,支持 修改闭包变量。如果没有 PyCellObject ,函数在运行时 直接修改 PyFrameObject函数返回就被回收了。借助 PyCellObject ,函数在运行时修改的是 ob_ref 。这样一来,就算函数返回,修改还是随函数而存在

示例理解#

1
2
3
4
5
6
7
8
9
lst = []
for i in arange(5):
def f ():
print (i)
print (f)
lst.append(f)

for f in lst:
f()

输出为

1
2
3
4
5
6
7
8
9
10
<function f at 0x000001E36692FE58>  //函数对象是动态生成的
<function f at 0x000001E366949048>
<function f at 0x000001E36694E798>
<function f at 0x000001E36694E4C8>
<function f at 0x000001E36694E708>
4
4
4
4
4
这是因为 闭包变量是通过 PyCellObject 间接引用,PyCellObject中的 ob_ref指针指向了i这个对象,而i最后变成了4。

若要正常输出0,1,2,3,4,应该怎么修改呢?

1
2
3
4
5
6
7
8
lst = []
for i in arange(5):
def f (i = i):
print (i)
lst.append(f)

for f in lst:
f()

1
2
3
4
5
0
1
2
3
4

这里的i是局部变量

装饰器#

前面我们了解了 嵌套函数和闭包,我们可以 让函数具备搭积木的魔法,例如:打印函数的执行时间。

事不宜迟,我们来实践一下,实现 timer 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
func(*args, **kwargs) #此处拿到了被装饰的函数func
time.sleep(2)#模拟耗时操作
long = time.time() - start
print(f'共耗时{long}秒。')
return wrapper #返回内层函数的引用

@timer
def add(a, b):
print(a+b)

add(1, 2) #正常调用add
timer被我们改造成了 装饰器,它接受被装饰函数为入参,返回内部 嵌套函数的引用(注意:此处并未执行函数),内部嵌套函数 wrapper持有被装饰函数的引用即func

“@”是Python的语法糖,它的作用类似于:

1
2
add = timer(add) #此处返回的是timer.<locals>.wrapper函数引用
add(1, 2)