要了解装饰器,首先我们来看看嵌套函数 ## 嵌套函数
1
2
3
4def adder(n):
def handler(x):
return n+x
return handler #返回的是一个函数的引用

作用域是一个 静态概念,由 Python 代码语法决定,与编译后产生的 代码对象 一一对应。作用域规定了能够被某个代码块访问的变量有哪些,但对变量具体的值则一概不关心。
一旦 Python 程序开始运行,虚拟机需要为 作用域中的变量 分配一定的 存储空间,这就是 名字空间 。名字空间依照作用域规则实现,它 决定了某个变量在运行时的取值,可以看做是 作用域在运行时的动态表现方式。
当 adder 函数执行时,作用域 A 在虚拟机中表现为 全局
名字空间,作用域 B 表现为 局部 名字空间:
1
2globals: adder
locals: n, handler1
2
3globals: 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(10) 为例,它是一个 handler 函数对象,闭包变量 n 值总是 10 。那么,内层函数是如何访问闭包作用域的呢?我们对函数代码对象进行反汇编,从中可以看出端倪:
1 | >>> add10 = adder(10) |
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 | lst = [] |
输出为
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
若要正常输出0,1,2,3,4,应该怎么修改呢?
1
2
3
4
5
6
7
8lst = []
for i in arange(5):
def f (i = i):
print (i)
lst.append(f)
for f in lst:
f()
1 | 0 |
这里的i是局部变量
装饰器#
前面我们了解了 嵌套函数和闭包,我们可以 让函数具备搭积木的魔法,例如:打印函数的执行时间。
事不宜迟,我们来实践一下,实现 timer 函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14def 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
“@”是Python的语法糖,它的作用类似于:
1
2add = timer(add) #此处返回的是timer.<locals>.wrapper函数引用
add(1, 2)
