什么是闭包
闭包closure
指的是引用了上层函数作用域的变量。当编程语言支持函数能够作为返回值,则闭包的实现则不可或缺,在函数内定义的子函数引用了父函数的变量,就是使用了语言的闭包特性。python中,一个通常会令人困惑的例子如下:
1 | def generate_funcs(): |
大多数初学者的直觉反应,程序最终会依次输出0到9,然而最终所有的函数都输出了9。因为闭包所绑定的是变量的名字,而不是变量的值,变量真正的值需要在运行时才能够确定。而当运行这几个内部函数时,变量i
已经走完了循环,定格在了9
这个值。如果想要绑定值,可以使用函数的默认参数来进行传递。
Python 2/3的闭包区别
在python 2中,是不允许修改闭包获得的变量值的,或者说,一旦你修改了值,python将不再认为该变量是外部作用域中的变量,而是内部作用域的新变量。这是因为python的变量赋值即声明,光凭借一句赋值语句无法判断变量的作用域,因此默认都为最内层作用域的变量。
python 2中,在没有写操作时,变量的查找依照LEGB的命名空间查找规则,即local->enclosing->global->built-in的查找顺序,当局部命名空间内不存在变量时,python会首先判断是否是闭包。需要注意的是,闭包的变量名是在编译期决定的,闭包获得的变量只与代码所在的上下文相关。
而在python 3中,新增了nonlocal
关键字,用该关键字声明后可对闭包变量进行赋值操作,因为不会再产生上述的歧义,即不会再认为是新变量的声明语句。
MAKE_CLOSRE指令
这里不得不吐槽一下,python 2中为了实现闭包,对使用了闭包的函数专门有一条创建指令MAKE_CLOSURE
,而普通函数,正如之前的博客中所属,是通过MAKE_FUNCTION
创建的。
1 | def func(): |
1 | TARGET(MAKE_CLOSURE) |
可以发现,MAKE_CLOSURE
和MAKE_FUNCTION
的实现几乎完全一样,唯一的不同就是多了PyFunction_SetClosure
的操作,可以简单理解为x.func_closure = v
。而PyFunction_SetClosure
所设置的值则是由指令LOAD_CLOSURE
获取的,这一条指令看上去也非常简单,只是将当前栈帧的freevars
中的一些值打包成了tuple
。(注意这里的freevars
对应的是co_cellvars
加上co_freevars
,真正加载到栈空间的其实是cellvars
)
1 | // freevars = f->f_localsplus + co->co_nlocals; |
freevars和cellvars
freevars
又是什么呢?在之前的代码中,freevars
被设置为了f->f_localsplus + co->co_nlocal
,似乎是栈空间的局部变量之后的一段空间,这一段空间在函数执行的时候会被初始化,具体的代码在PyEval_EvalCodeEx
中。
1 | /* Allocate and initialize storage for cell vars, and copy free |
可以发现,freevars
地址存放的变量实际上和func_code
域中的code_freevars
和code_cellvars
变量相关。其中:
code_cellvars
存放了内层函数引用的当前作用域的变量名,即变量名a
在func.func_code.code_cellvars
中code_freevars
存放了当前作用域引用的上层作用域的变量名, 即变量名a
在g.func_code.code_freevars
中
注意到,在函数开始执行之前,co_cellvars
对应的值是不确定的,通常都被初始化为空(引用函数参数作为闭包的co_cellvars
会有初始值),而co_freevars
对应的值已经确定了,其实就来自于func_closure
域,结合之前对LOAD_CLOSURE
指令的分析,我们发现func_closure
域其实就来自于上一个栈帧的co_cellvars
。
rankdir = LR;
splines = false;
{
rank = same;
outer_func [shape = box; label=”outer stack”; group=group1];
“upper_func” [
label = “《f0》 locals| | | 《f1》 cellvars | | | 《f2》 freevars | | “
shape = “record”
group = group2
];
}
subgraph cluster_0 {
“func_attr” [
label = “| |《f》func_closure| |”
shape = “record”;
];
label = “PyFunctionObject”;
}
{
rank=same;
inner_func [shape = box; label=”inner stack”; group=group1];
“cur_func” [
label = “《f0》 locals| | | 《f1》 cellvars | | | 《f2》 freevars | |”
shape = “record”
group = group2
];
}
runtime [shape=box;]
cur_func:f1:ne -> runtime:nw [style=dotted;arrowtail=empty;dir=back];
cur_func:f2:ne -> runtime:sw [style=dotted;arrowtail=empty;dir=back];
upper_func:f1:ne -> func_attr:f:nw [style=dotted;arrowhead=empty];
upper_func:f2:ne -> func_attr:f:sw [style=dotted;arrowhead=empty];
func_attr:f:ne -> cur_func:f2:nw [style=dotted;arrowhead=empty];
func_attr:f:se -> cur_func:sw [style=dotted;arrowhead=empty];
outer_func:se -> func_attr:nw [label=”create function”; lhead=cluster_0];
func_attr:ne -> inner_func:sw [label=”call function”; lhead=cluster_0];
}
PyCellObject
需要注意的是,cellvars
和freevars
中存放的变量并不是简单的PyObject *
,而是通过PyCellObject
进行了简单的封装,对其的加载和赋值也都有单独的指令,这样做的原因是为了实现对变量引用的闭包。如上面的图例所展现的那样,在创建函数的时候就会将当前栈帧的cellvars
拷贝到func_closure
中,如果创建函数后重新对闭包引用的变量赋值,不使用PyCellObject
就无法捕获到这一变化。
1 | typedef struct { |
用类似的python例子来类比,PyCellObject
就像一个容器,对应的就是绑定的变量名,而变量名对应的对象的变化,也就是PyCellObject
持有的引用的变化。
1 | # closure by value |