Python虚拟机之函数机制
Posted 北洛
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python虚拟机之函数机制相关的知识,希望对你有一定的参考价值。
函数执行时的名字空间
在Python虚拟机函数机制之无参调用(一)这一章中,我们对Python中的函数调用机制有个大概的了解,在此基础上,我们再来看一些细节上的问题。在执行MAKE_FUNCTION指令时,调用了PyFunction_New方法,这个方法有一个参数是globals,这个globals最终将称为与函数f对应的PyFrameObject中的global名字空间——f_globals
ceval.c
case MAKE_FUNCTION: v = POP(); /* code object */ x = PyFunction_New(v, f->f_globals); Py_DECREF(v); /* XXX Maybe this should be a separate opcode? */ 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;
# cat demo.py def f(): print("Function") f() # python2.5 …… >>> source = open("demo.py").read() >>> co = compile(source, "demo.py", "exec") >>> import dis >>> dis.dis(co) 1 0 LOAD_CONST 0 (<code object f at 0x7fd9831c3648, file "demo.py", line 1>) 3 MAKE_FUNCTION 0 6 STORE_NAME 0 (f) 5 9 LOAD_NAME 0 (f) 12 CALL_FUNCTION 0 15 POP_TOP 16 LOAD_CONST 1 (None) 19 RETURN_VALUE >>> from demo import f Function >>> dis.dis(f) 2 0 LOAD_CONST 1 (\'Function\') 3 PRINT_ITEM 4 PRINT_NEWLINE 5 LOAD_CONST 0 (None) 8 RETURN_VALUE
在Python虚拟机中的一般表达式(三)中,我们介绍了LOAD_NAME这条指令,这条指令在执行时会依次从三个PyDictObject对象进行搜索,搜索顺序是:f_locals、f_globals、f_builtins。在PyFunction_New时传入的globals将成为在新的栈帧中执行函数的global名字空间。在MAKE_FUNCTION中,我们看到传入的globals参数为当前PyFrameObject对象中的f_globals。这意味着,在执行demo.py的字节码指令时的global名字空间,与执行函数f的字节码序列时的global名字空间实际上是同一个名字空间,这个名字空间通过PyFunctionObject的携带,和字节码指令对应的PyCodeObject对象一起被传入到新的栈帧中
下面,让我们修改MAKE_FUNCTION指令和call_function的实现,将global名字空间的地址和内容输出
ceval.c
case MAKE_FUNCTION: v = POP(); /* code object */ char *v_co_name = PyString_AsString(((PyCodeObject *)v)->co_name); if(strcmp(v_co_name, "f") == 0) { if (stream == NULL || stream == Py_None) { w = PySys_GetObject("stdout"); if (w == NULL) { PyErr_SetString(PyExc_RuntimeError, "lost sys.stdout"); err = -1; } } //打印globals名字空间的地址 printf("[MAKE_FUNCTION]:f_globals addr:%p\\n", f->f_globals); //打印globals名字空间的内容 printf("[MAKE_FUNCTION]:"); PyFile_WriteObject(f->f_globals, w, Py_PRINT_RAW); printf("\\n"); stream = NULL; } x = PyFunction_New(v, f->f_globals); …… …… static PyObject * call_function(PyObject ***pp_stack, int oparg) { int na = oparg & 0xff; int nk = (oparg>>8) & 0xff; int n = na + 2 * nk; PyObject **pfunc = (*pp_stack) - n - 1; PyObject *func = *pfunc; PyObject *x, *w; char *func_name = PyEval_GetFuncName(func); if (strcmp(func_name, "f") == 0) { PyObject *std = PySys_GetObject("stdout"); //获取函数所对应的global名字空间 PyObject *func_globals = (PyCodeObject *)PyFunction_GET_GLOBALS(func); //打印globals名字空间的地址 printf("[call_function]:func_globals addr:%p\\n", func_globals); //打印globals名字空间的内容 printf("[call_function]:"); PyFile_WriteObject(func_globals, std, Py_PRINT_RAW); printf("\\n"); } …… }
然后,我们执行一下demo1.py这个文件
# cat demo1.py a = 1 b = 3 def f(): print("Function f") def g(): print("Function g") f() # python2.5 demo1.py [MAKE_FUNCTION]:f_globals addr:0x2237740 [MAKE_FUNCTION]:{\'a\': 1, \'b\': 3, \'__builtins__\': <module \'__builtin__\' (built-in)>, \'__file__\': \'demo1.py\', \'__name__\': \'__main__\', \'__doc__\': None} [call_function]:func_globals addr:0x2237740 [call_function]:{\'a\': 1, \'b\': 3, \'g\': <function g at 0x7f54708c7de8>, \'f\': <function f at 0x7f54708c7b18>, \'__builtins__\': <module \'__builtin__\' (built-in)>, \'__file__\': \'demo1.py\', \'__name__\': \'__main__\', \'__doc__\': None} Function f
可以看到,MAKE_FUNCTION中和call_function中的global名字空间的地址是一样的,demo1.py中所定义的符号都包含在global名字空间中,使得函数f可以使用a、b两个变量,正是依赖于global名字空间的传递,才使得函数f可以使用函数f以外的符号。现在,让我们分别看下[MAKE_FUNCTION]和[call_function]中的globals内容,我们会发现,前者没有函数f和g,后者有函数f和g,加上两者的地址是相同的。这说明在字节码指令执行的时候,一定会把函数f和g放入到global名字空间,否则,函数在global中找不到自身的定义,无法实现递归,虽然我们的函数f在这里并没有递归
那么,函数f和g是在何时偷偷溜进global名字空间呢?我们用dis模块来查看一下demo1.py源代码对应的字节码指令
[root@10-19-127-65 python]# python2.5 …… >>> source = open("demo1.py").read() >>> co = compile(source, "demo1.py", "exec") >>> import dis >>> dis.dis(co) 1 0 LOAD_CONST 0 (1) 3 STORE_NAME 0 (a) 2 6 LOAD_CONST 1 (3) 9 STORE_NAME 1 (b) 5 12 LOAD_CONST 2 (<code object f at 0x7f5d0aa74648, file "demo1.py", line 5>) 15 MAKE_FUNCTION 0 18 STORE_NAME 2 (f) 9 21 LOAD_CONST 3 (<code object g at 0x7f5d0aa74918, file "demo1.py", line 9>) 24 MAKE_FUNCTION 0 27 STORE_NAME 3 (g) 13 30 LOAD_NAME 2 (f) 33 CALL_FUNCTION 0 36 POP_TOP 37 LOAD_CONST 4 (None) 40 RETURN_VALUE
我们看到"15 MAKE_FUNCTION 0"和"24 MAKE_FUNCTION 0"这两句指令,这两句都是执行def语句创建PyFunctionObject对象,MAKE_FUNCTION指令创建PyFunctionObject对象后便将其压入栈,显然,在global名字空间建立符号f和g与PyFunctionObject对象的映射不在MAKE_FUNCTION。所以我们往后找,这两句指令的后面又分别跟着"18 STORE_NAME 2 (f)"和"27 STORE_NAME 3 (g)",会不会是在这里建立符号与函数对象的映射呢?我们看看STORE_NAME的实现:
ceval.c
case STORE_NAME: w = GETITEM(names, oparg); v = POP(); if ((x = f->f_locals) != NULL) { if (PyDict_CheckExact(x)) err = PyDict_SetItem(x, w, v); else err = PyObject_SetItem(x, w, v); Py_DECREF(v); if (err == 0) continue; break; } PyErr_Format(PyExc_SystemError, "no locals found when storing %s", PyObject_REPR(w)); break;
这里我们看到,STORE_NAME会对local名字空间做符号和其值的映射,但并不是我们之前所说的global名字空间啊!所以,到底是不是在这里做符号与函数的映射呢?答案是:符号与函数的映射,正是在STORE_NAME完成的。这里也暴露一个信息,demo1.py执行时对应的local名字空间和global名字空间实际上是一个对象,想想也是这个道理,因为函数的local名字空间存储的是函数内的局部变量,global存储的是函数之外的变量,那么一个脚本本身所对应的local名字空间存储的是脚本本身的变量,那么global名字空间呢?这里没得选,只能和脚本本身的local名字空间共同使用一个PyDictObject对象了
以上是关于Python虚拟机之函数机制的主要内容,如果未能解决你的问题,请参考以下文章