Python虚拟机函数机制之闭包和装饰器(七)
函数中局部变量的访问
在完成了对函数参数的剖析后,我们再来看看,在Python中,函数的局部变量时如何实现的。前面提到过,函数参数也是一种局部变量。所以,其实局部变量的实现机制与函数参数的实现机制是完全一样的。这个“一样”是什么意思呢?
之前我们剖析过Python虚拟机的一些指令,如果要访问一个变量,应该使用LOAD_NAME指令,应该依照local、global、builtin这三个名字空间里去检索变量名所对应的变量值。然后在调用函数时,Python虚拟机通过PyFrame_New创建新的PyFrameObject对象时,那个至关重要的local对象并没有创建
在对Python虚拟机机制分析的过程中,我们得知当直接调用一个脚本时,脚本对应的PyCodeObject对象中f_locals和f_globals实际上是同一个对象。那么在函数执行时,对变量的读写决不能反应在f_globals上,否则,一个函数执行完,函数外部不就可以知道函数的局部变量吗?所以,函数是如何对局部变量进行读写呢?我们来对一个带有局部变量的函数用dis模块进行字节码指令的解释
>>> def f(a, b): ... c = a + b ... print(c) ... >>> import dis >>> dis.dis(f) 2 0 LOAD_FAST 0 (a) 3 LOAD_FAST 1 (b) 6 BINARY_ADD 7 STORE_FAST 2 (c) 3 10 LOAD_FAST 2 (c) 13 PRINT_ITEM 14 PRINT_NEWLINE 15 LOAD_CONST 0 (None) 18 RETURN_VALUE
这里,我们并未发现有LOAD_NAME或者STORE_NAME这样对名字空间的读写指令,取而代之的LOAD_FAST和STORE_FAST,这是不是印证我们之前所说的,局部变量的实现机制与函数参数的实现机制是完全一样的,局部变量的读写也是在f_localsplus上
为什么函数的实现中没有使用local名字空间呢?这是因为函数中的局部变量总是固定不变的,所以在编译时就能确定局部变量使用的内存空间的位置,也能确定访问局部变量的字节码指令应该如何访问内存。有了这些信息,Python就能使用静态的方法来实现局部变量,而不需要借助于动态地查找PyDictObject对象的技术。毕竟,函数调用实在太普遍了,静态的方法可以极大地提高函数的执行效率
嵌套函数
在Python中,有一个核心的概念叫名字空间,一段代码的执行的结果不光取决于代码中的符号,更多地是取决于代码中符号的语义,而这个运行时语义正是由名字空间所决定的。名字空间是在运行时由Python虚拟机动态维护的,但有时,我们希望名字空间静态化。换句话说,我们希望代码不受名字空间变化的影响,始终保持一致的行为和结果。这样做有什么意义呢?
假如我们想要定一个基准值,然后将许多值与这个值进行比较,最简单的方法就是写一个函数:
>>> def compare(base, value): ... return value > base ... >>> compare(10, 5) False >>> compare(10, 20) True
我们以10作为基准值,与5和20进行比较,但是会发现,每次调用函数时,都必须多传一个10。于是,Python提供了嵌套函数
>>> base = 1 >>> def get_compare(base): ... def real_compare(value): ... return value > base ... return real_compare ... >>> compare_with_10 = get_compare(10) >>> compare_with_10(5) False >>> compare_with_10(20) True
如上述代码,我们只设置了一次基准值。此后,在每次进行比较操作时,尽管调用的实际函数real_compare的local名字空间并没有base,而get_compare函数之外的global名字空间中有"base = 1",但是函数调用的结果显示,real_compare以我们传入的10作为base,而不是以get_compare函数之外的base作为基准书
也就是说,在real_compare这个函数作为返回值被传递给compare_with_10的时候,有一个名字空间已经与real_compare紧紧地绑定在一起,在执行real_compare的代码时,这个名字空间又恢复了,这就是将名字空间静态化的方法。这个名字空间与函数捆绑后的结果被称为一个闭包。在前面我们看到,PyFunctionObject是Python虚拟机专门为包裹字节码指令、global名字空间、默认参数值准备的大包袱,都能在PyFunctionObject中找到其位置,同样,Python中的闭包也是通过PyFunctionObject对象来实现
实现闭包的基石
我们先来看看PyCodeObject、PyFunctionObject、PyFrameObject这些我们熟悉的对象中,与闭包有关的属性。闭包的创建通常是利用嵌套函数来完成,在PyCodeObject中,与嵌套函数相关的属性是co_freevars、co_cellvars。两者具体含义如下:
- co_freevars:通常是一个tupple,保存嵌套的作用域中使用的变量名集合
- co_cellvars:通常是一个tupple,保存使用了的外层作用域中的变量名集合
考虑下面的代码:
# cat demo4.py def get_func(): a = 1 value = "inner" def inner_func(): print(value) return inner_func
很显然,上述的代码会编译出3个PyCodeObject对象,其中有两个,一个与函数get_func对应,一个与函数inner_func对应。那么,与get_func对应的PyCodeObject对象中co_cellvars就应该包含字符串"value",因为其嵌套作用域(inner_func的作用域)中使用了这个符号。同理,与函数inner_func对应的PyCodeObject对象中的co_freevars中应该也有字符串"value"。下面,我们来证实一下:
>>> source = open("demo4.py").read() >>> co = compile(source, "demo4.py", "exec") >>> co.co_consts (<code object get_func at 0x7efc8c8c1b70, file "demo4.py", line 1>, None)
demo4.py对应的PyCodeObject中,co_consts这个元组第一个对象就是get_func对应的PyCodeObject,我们将其取出,然后看一下其中的co_cellvars
>>> get_func_co = co.co_consts[0] >>> get_func_co.co_name 'get_func' >>> get_func_co.co_cellvars ('value',)
从前面的demo4.py可以知道,尽管get_func中除去变量value,还有一个变量a,但是a没有函数内部的嵌套函数使用,所以co_cellvars只有符号value,没有符号a。我们都知道,PyCodeObject是可以嵌套PyCodeObject的,既然get_func的PyCodeObject对象被我们取出,不妨,我们再取出inner_func对应的PyCodeObject,但在这之前,我们要先看一下,inner_func的PyCodeObject,到底处于get_func对应的PyCodeObject的co_consts哪个位置
>>> get_func_co.co_consts (None, 1, 'inner', <code object inner_func at 0x7efc8c8c1cd8, file "demo4.py", line 5>) >>> inner_func_co = get_func_co.co_consts[3] >>> inner_func_co.co_freevars ('value',)
可以看到,inner_func对应的PyCodeObject中,co_freevars果然有符号value
以上,便是PyCodeObject中与闭包相关的属性。下面,我们再来看看PyFrameObject对象中,与闭包属性相关的对象。其实在这里,只有一个对象和闭包相关,就是我们的老朋友f_localsplus
在PyFrame_New函数中,有这样一段代码:
ncells = PyTuple_GET_SIZE(code->co_cellvars); nfrees = PyTuple_GET_SIZE(code->co_freevars); extras = code->co_stacksize + code->co_nlocals + ncells + nfrees;
extras正是f_localsplus指向的那片内存的大小,这片内存是属于运行时栈、局部变量、cell对象(co_cellvars)、和free对象(co_freevars)。下面,展现一下f_localsplus的布局:
图1-1 f_localsplus的完整内存布局
闭包的实现
在介绍完闭包的基石后,我们就可以开始追踪闭包的具体实现过程了。但是我们好像还忘了一件事,之前说过与闭包有关的对象,除了PyCodeObject、PyFrameObject,还有一个PyFunctionObject。没关系,我们很快就会了解到PyFunctionObject与闭包那些不能不说的事。不过首先,我们还是要来看一下demo5.py编译后的字节码指令:
其实,demo5.py相比demo4.py,仅仅是把get_func函数中的变量a给移除
# cat demo5.py def get_func(): value = "inner" def inner_func(): print(value) return inner_func show_value = get_func() show_value()
有了demo5.py,我们就可以逐层分析PyCodeObject对象,闭包指令是长什么样的
首先,我们先得到demo5.py对应的PyCodeObject对象
>>> source = open("demo5.py").read() >>> co = compile(source, "demo5.py", "exec") >>> import dis >>> dis.dis(co) 1 0 LOAD_CONST 0 (<code object get_func at 0x255d120, file "demo5.py", line 1>) 3 MAKE_FUNCTION 0 6 STORE_NAME 0 (get_func) 10 9 LOAD_NAME 0 (get_func) 12 CALL_FUNCTION 0 15 STORE_NAME 1 (show_value) 11 18 LOAD_NAME 1 (show_value) 21 CALL_FUNCTION 0 24 POP_TOP 25 LOAD_CONST 1 (None) 28 RETURN_VALUE
demo5.py的指令序列我们已经很熟悉了,重点不是在这,而是在get_func和inner_func的指令序列
get_func指令序列
>>> co.co_consts (<code object get_func at 0x255d120, file "demo5.py", line 1>, None) >>> get_func_co = co.co_consts[0] >>> get_func_co.co_flags 3 >>> dis.dis(get_func_co) 2 0 LOAD_CONST 1 ('inner') 3 STORE_DEREF 0 (value) 4 6 LOAD_CLOSURE 0 (value) 9 BUILD_TUPLE 1 12 LOAD_CONST 2 (<code object inner_func at 0x7efc8c8c1918, file "demo5.py", line 4>) 15 MAKE_CLOSURE 0 18 STORE_FAST 0 (inner_func) 7 21 LOAD_FAST 0 (inner_func) 24 RETURN_VALUE
inner_func指令序列
>>> get_func_co.co_consts (None, 'inner', <code object inner_func at 0x7efc8c8c1918, file "demo5.py", line 4>) >>> inner_func_co = get_func_co.co_consts[2] >>> inner_func_co.co_flags 19 >>> dis.dis(inner_func_co) 5 0 LOAD_DEREF 0 (value) 3 PRINT_ITEM 4 PRINT_NEWLINE 5 LOAD_CONST 0 (None) 8 RETURN_VALUE
在fast_function中,我们先前有介绍过一个快速通道,但这个快速通道不是任何函数都能进的,在进之前要满足若干条件,其中一个条件就是co->co_flags == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE),即co_flags为67。可惜get_func和inner_func的co_flags都不是67,注定只能乖乖走PyEval_EvalCodeEx
如果当前PyCodeObject的co_cellvars的长度不为0,将进入下面代码的分支,Python虚拟机会如同处理默认参数一样,将co_cellvars中的东西拷贝到新创建的PyFrameObject的f_localsplus中
ceval.c
PyObject * PyEval_EvalCodeEx(PyCodeObject *co, PyObject *globals, PyObject *locals, PyObject **args, int argcount, PyObject **kws, int kwcount, PyObject **defs, int defcount, PyObject *closure) { …… if (PyTuple_GET_SIZE(co->co_cellvars)) { int i, j, nargs, found; char *cellname, *argname; PyObject *c; nargs = co->co_argcount; if (co->co_flags & CO_VARARGS) nargs++; if (co->co_flags & CO_VARKEYWORDS) nargs++; for (i = 0; i < PyTuple_GET_SIZE(co->co_cellvars); ++i) { //[1]:获得被嵌套函数共享的符号名 cellname = PyString_AS_STRING( PyTuple_GET_ITEM(co->co_cellvars, i)); found = 0; for (j = 0; j < nargs; j++) { argname = PyString_AS_STRING( PyTuple_GET_ITEM(co->co_varnames, j)); if (strcmp(cellname, argname) == 0) { c = PyCell_New(GETLOCAL(j)); if (c == NULL) goto fail; GETLOCAL(co->co_nlocals + i) = c; found = 1; break; } } //处理被嵌套函数共享外层函数的默认参数 if (found == 0) { c = PyCell_New(NULL); if (c == NULL) goto fail; SETLOCAL(co->co_nlocals + i, c); } } } …… }
在上述代码的[1]处,Python虚拟机获得了被内层嵌套函数引用的符号名,在我们的例子中,就是获得了一个字符串"value"。这里的found是被内层嵌套函数引用的符号是否已经与某个值绑定的标识,或者说与某个对象建立了约束关系。只有在内层嵌套函数引用的是外层函数的一个有默认值的参数时,这个标识才可能为1。对于我们的例子,found一定为0。因为get_func所对应的PyCodeObject中,co_varnames寻找不到符号"value"。所以Python虚拟机接下来会创建cell对象——PyCellObject
cellobject.c
typedef struct { PyObject_HEAD PyObject *ob_ref; /* Content of the cell or NULL when empty */ } PyCellObject;
这个对象非常简单,仅仅维护一个ob_ref,指向一个PyObject对象,我们来看看PyCellObject的创建代码
cellobject.c
PyObject * PyCell_New(PyObject *obj) { PyCellObject *op; op = (PyCellObject *)PyObject_GC_New(PyCellObject, &PyCell_Type); if (op == NULL) return NULL; op->ob_ref = obj; Py_XINCREF(obj); _PyObject_GC_TRACK(op); return (PyObject *)op; }
在我们的例子中,创建的PyCellObject对象维护的ob_ref指向了NULL,也就是说,现在还不知道符号value到底是什么东西,那么什么时候才能知道呢?在value = "inner"这个赋值语句执行的时候。随后,在PyEval_EvalCodeEx中,这个cell对象被拷贝到新创建的PyFrameObject对象的f_localsplus中。值的注意的是,这个对象被拷贝到的位置是co_co_nlocals + i。说明在n_localsplus中,cell对象的位置是在局部变量之后的,这完全符合图1-1所示的内存布局
在处理co_cellvars时,有一个奇怪的地方,在我们创建PyCellObject对象的过程中,代码[1]处的cellname完全忽略了。实际上,这和前面分析到的Python函数机制对局部变量符号的访问方式从对dict的查找变为对list的索引是一个道理。在get_func函数执行的过程中,对value这个cell变量的访问将通过基于索引访问f_localsplus完成,因为完全不需要再知道cellname了。这个cellname实际上是在处理内层嵌套函数引用外层函数的默认参数时产生的
在处理了cell对象之后,Python虚拟机将进入PyEval_EvalFrameEx,从而正式开始对函数get_func的调用过程,这里,我们再贴一下get_func的字节码指令序列
>>> dis.dis(get_func_co) 2 0 LOAD_CONST 1 ('inner') 3 STORE_DEREF 0 (value) 4 6 LOAD_CLOSURE 0 (value) 9 BUILD_TUPLE 1 12 LOAD_CONST 2 (<code object inner_func at 0x7efc8c8c1918, file "demo5.py", line 4>) 15 MAKE_CLOSURE 0 18 STORE_FAST 0 (inner_func) 7 21 LOAD_FAST 0 (inner_func) 24 RETURN_VALUE
首先执行"0 LOAD_CONST 1"指令将PyStringObject对象"inner"压入到运行时栈,然后Python虚拟机开始执行一条对我们是全新的字节码指令——STORE_DEREF
ceval.c
PyObject * PyEval_EvalFrameEx(PyFrameObject *f, int throwflag) { …… freevars = f->f_localsplus + co->co_nlocals; …… }
ceval.c
case STORE_DEREF: w = POP(); x = freevars[oparg]; PyCell_Set(x, w); Py_DECREF(w); continue;
从运行时栈弹出的是PyStringObject对象"inner",而从f_localsplus中取得的是PyCellObject对象。原来,STORE_DEREF是要设置PyCellObject对象中的ob_ref
cellobject.c
#define PyCell_SET(op, v) (((PyCellObject *)(op))->ob_ref = v) int PyCell_Set(PyObject *op, PyObject *obj) { if (!PyCell_Check(op)) { PyErr_BadInternalCall(); return -1; } Py_XDECREF(((PyCellObject*)op)->ob_ref); Py_XINCREF(obj); PyCell_SET(op, obj); return 0; }
这样一来,f_localsplus中的cell对象就发生了变化,如图1-2
图1-2 设置cell对象之后的get_func函数的PyFrameObject对象
现在在get_func的环境中我们知道了value符号对应着一个PyStringObject对象,但是闭包的作用是将这个约束进行冻结,使得在嵌套函数inner_func被调用时还能使用这个约束。这一次,又需要用到PyFunctionObject这个对象了。在执行demo5.py中def inner_func()表达式时,Python虚拟机就将(value, "inner")这个约束塞到PyFunctionObject中
ceval.c
case LOAD_CLOSURE: x = freevars[oparg]; Py_INCREF(x); PUSH(x); if (x != NULL) continue; break;
"6 LOAD_CLOSURE 0"指令将刚刚放置好的PyCellObject对象取出,并压入运行时栈,接着执行"9 BUILD_TUPLE 1"指令将PyCellObject对象打包进一个tupple中,显然,这个tupple可以放置多个PyCellObject对象。不过,我们的例子只有一个PyCellObject对象
随后,Python虚拟机通过执行"12 LOAD_CONST 2"指令将inner_func对应的PyCodeObject对象也压入到运行时栈,接着以一个"15 MAKE_CLOSURE 0"指令完成约束与PyCodeObject的绑定
ceval.c
case MAKE_CLOSURE: { v = POP(); //获得PyCodeObject对象 x = PyFunction_New(v, f->f_globals);//绑定global名字空间 Py_DECREF(v); if (x != NULL) { v = POP();//获得tupple,其中包含PyCellObject对象的集合 err = PyFunction_SetClosure(x, v);//绑定约束集合 Py_DECREF(v); } //处理拥有默认值的参数 if (x != NULL && oparg > 0) { v = PyTuple_New(oparg); if (v == NULL) { Py_DECREF(x); x = NULL; break; } while (--oparg >= 0) { w = POP(); PyTuple_SET_ITEM(v, oparg, w); } err = PyFunction_SetDefaults(x, v); Py_DECREF(v); } PUSH(x); break; }
表达式"def inner_func()"所对应的最后一条"18 STORE_FAST 0"指令将所创建的PyFunctionObject对象放置到了f_localsplus中。这样,f_localsplus又发生了变化
图1-3 设置function对象之后的get_func函数中的PyFrameObject对象
在get_func的最后,这个新建的PyFunctionObject对象将作为返回值返回给上一个栈帧,并被压入到该栈帧的运行时栈中
使用闭包(closure)
闭包是在get_func中创建的,而对于闭包的使用,则是在inner_func中。在执行"show_value()"对应的CALL_FUNCTION时,和inner_func对应的PyCodeObject中的co_flags里包含了CO_NESTED,所以在fast_function中依旧不能通过快速通道的验证,还是要进入到PyEval_EvalCodeEx
这里,我们再看一下inner_func对应的字节码指令序列
>>> dis.dis(inner_func_co) 5 0 LOAD_DEREF 0 (value) 3 PRINT_ITEM 4 PRINT_NEWLINE 5 LOAD_CONST 0 (None) 8 RETURN_VALUE
我们已经看到,inner_func对应的PyCodeObject这种co_freevars里有引用外部作用域的符号名,在PyEval_EvalCodeEx中,就会对这个co_freevars进行处理
ceval.c
PyObject * PyEval_EvalCodeEx(PyCodeObject *co, PyObject *globals, PyObject *locals, PyObject **args, int argcount, PyObject **kws, int kwcount, PyObject **defs, int defcount, PyObject *closure) { …… if (PyTuple_GET_SIZE(co->co_freevars)) { int i; for (i = 0; i < PyTuple_GET_SIZE(co->co_freevars); ++i) { PyObject *o = PyTuple_GET_ITEM(closure, i); Py_INCREF(o); freevars[PyTuple_GET_SIZE(co->co_cellvars) + i] = o; } } …… }
其中,closure变量作为最后一个函数参数传递进来,我们看看在fast_function中到底传进来什么
//funcobject.c #define PyFunction_GET_CLOSURE(func) \ (((PyFunctionObject *)func) -> func_closure) //ceval.c static PyObject * fast_function(PyObject *func, PyObject ***pp_stack, int n, int na, int nk) { …… return PyEval_EvalCodeEx(co, globals, (PyObject *)NULL, (*pp_stack) - n, na, (*pp_stack) - 2 * nk, nk, d, nd, PyFunction_GET_CLOSURE(func)); }
原来传进来的就是在PyFunctionObject对象中与PyCodeObject对象绑定的装满了PyCellObject对象的tupple,所以在PyEval_EvalCodeEx中,进行的动作就是将tupple中一个个PyCellObject对象放入到f_localsplus中相应的位置。处理完closure后,inner_func对应的PyFrameObject中的f_localsplus如图1-4所示
图1-4 设置cell对象之后的inner_func函数的PyFrameObject对象
所以,在inner_func调用过程中,当引用到外层作用域的符号时,一定是到f_localsplus中的free变量区域中获得符号对应的值。这正是inner_func函数中"print(value)"表达式对应的第一条字节码指令"0 LOAD_DEREF 0"
ceval.c
case LOAD_DEREF: x = freevars[oparg];//获得PyCellObject对象 w = PyCell_Get(x);//获得PyCellObject.ob_ref指向的对象 if (w != NULL) { PUSH(w); continue; } ……
装饰器(Decorator)
在closure技术的基础上,Python实现的装饰器(Decorator),来看下面的例子:
# cat demo6.py def should_say(fn): def say(*args): print("say something...") fn(*args) return say @should_say def func(): print("in func") func() # python2.5 demo6.py say something... in func
实际上,我们可以完全不用decorator,而实现同样的效果,只需要对demo6.py做小小的修改
# cat demo7.py def should_say(fn): def say(*args): print("say something...") fn(*args) return say def func(): print("in func") func = should_say(func) func() # python2.5 demo7.py say something... in func
会发现demo6.py和demo7.py的输出结果相同。实际上,基于上面对closure的剖析,装饰器的行为就很好理解了,装饰器只是用一个函数来包装另一个函数,类似"func = should_say(func)"的形式。现在,我们来看看demo6.py和demo7.py中部分编译结果
demo6.py字节码指令序列
@should_say def func(): //字节码指令序列 9 LOAD_NAME 0 (should_say) 12 LOAD_CONST 1 (<code object func at 0x255d3f0, file "demo6.py", line 9>) 15 MAKE_FUNCTION 0 18 CALL_FUNCTION 1 21 STORE_NAME 1 (func) print("in func")
demo7.py字节码指令序列
def func(): //字节码指令序列 9 LOAD_CONST 1 (<code object func at 0x255d558, file "demo7.py", line 9>) 12 MAKE_FUNCTION 0 15 STORE_NAME 1 (func) print("in func") func = should_say(func) //字节码指令序列 18 LOAD_NAME 0 (should_say) 21 LOAD_NAME 1 (func) 24 CALL_FUNCTION 1 27 STORE_NAME 1 (func)
在demo7.py中,"15 STORE_NAME 1"和"21 LOAD_NAME 1"这两条字节码指令互为逆运算,可以删除。如此一来,demo7.py编译后的字节码指令序列和demo6.py编译后的字节码指令序列,除了"LOAD_NAME 0"的位置不同外,其余的都完全相同。