int main()
{
g();
}
其中图1绿色部分对应的是g的栈帧,而黄色部分对应的是f的栈帧。对于一个函数而言,其所有的局部变量和操作都在自己的栈帧中完成,而函数之间的调用则通过创建新的栈帧完成,当然,在函数调用发生时,系统会保存上一个栈帧的栈指针esp和帧指针ebp。大致上,这就是可执行文件在x86机器上的运行原理,而Python正是在执行引擎中通过不同的实现方式模拟了这一原理。
前面我们已经知道,Python源代码经过编译之后,所有的byte code以及程序的其他信息都存放在PyCodeObject对象中,那么Python的执行引擎是否就是在这个PyCodeObject对象上进行所有的动作呢?呃,是,又不是。PyCodeObject中包含了最关键的byte code以及关于程序的所有信息。然而有一点,PyCodeObject没有包含,也不可能包含。这就是执行环境。
什么是执行环境呢,考虑下面的一个例子:
i = ‘Python’
**def** f():
i = 999
**print** i #1
f()
**print** i #2
在1和2两个地方,都进行了同样的动作,即print i,显然,它们所对应的字节码是相同的,但是这两条语句的执行效果是不同的。这样的结果正是在执行环境的影响下产生的。在执行1处的print时,执行环境中,i的值为999;而在执行2处的print时,执行环境中i的值为“Python”。这种同样的名字对应不同的值,甚至不同的类型的情况,必须在运行时动态地被捕捉和维护。这些则不可能在PyCodeObject中被静态地存储。
联想到x86运行程序的机理,我们可以这样来考虑。当Python调用函数f时,会在当前的运行环境之外重新创建一个新的运行环境,在这个新的运行环境中,有一个新的名字为“i”的对象,这个新的运行环境实际上可以看成是一个新的栈帧。所以在Python真正执行的时候,它的执行引擎实际上面对的并不是一个PyCodeObject对象,而是另一个家伙——PyFrameObject,这个东西就是Python对栈帧的模拟,你看,它的名字中还有个Frame呢。
当然,对于Python而言,PyFrameObject对象不是一个我们在x86机器上看到的那个简简单单的栈帧,它实际上包含了其他更多的信息,请看:
[frameobject.h]
**typedef struct** \_frame **{**
PyObject\_VAR\_HEAD
**struct** \_frame \*f\_back; /\* previous frame, or NULL \*/
PyCodeObject \*f\_code; /\* code segment \*/
PyObject \*f\_builtins; /\* builtin symbol table (PyDictObject) \*/
PyObject \*f\_globals; /\* global symbol table (PyDictObject) \*/
PyObject \*f\_locals; /\* local symbol table (any mapping) \*/
PyObject \*\*f\_valuestack; /\* points after the last local \*/
PyObject \*\*f\_stacktop;
……
**int** f\_lasti;