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"的位置不同外,其余的都完全相同。 

 

posted @ 2018-08-26 16:11  北洛  阅读(525)  评论(0编辑  收藏  举报