Python虚拟机的运行原理和exe文件在x86架构的CPU中执行的原理类似。都是通过栈帧的方式来执行的(其实就是一个栈指针在一个栈中移来移去,然后不停地出栈入栈,不停创建栈帧),至于具体怎么运行的比较麻烦,我就不说的,自己看书去。简单来说栈帧和python中命名空间是对应的。一个栈帧对应一个命名空间,所以一个栈帧也对应一个PyCodeObject对象。在Python中栈帧的抽象叫做PyFrameObject。
[frameobject.h] typedef struct _frame { PyObject_VAR_HEAD struct _frame *f_back; // 执行环境链上的前一个frame PyCodeObject *f_code; // PyCodeObject对象 PyObject *f_builtins; // builtin命名空间 PyObject *f_globals; // global命名空间 PyObject *f_locals; // local命名空间 PyObject **f_valuestack; // 运行时栈的栈底位置 PyObject **f_stacktop; // 运行时栈的栈顶位置 ... int f_lasti; // 上一条字节码指令在f_code中的偏移位置 int f_lineno; // 当前字节码对应的源代码行 PyObject *f_localsplus[1]; // 动态内存。(局部变量 + cell对象集合 + free对象集合 + 运行时栈) } PyFrameObject;
书本上说这里的f_code存放的是一个待执行的PyCodeObject对象,而不是当前正在执行的PyCodeObject,我表示有点小怀疑,待会我去验证一下。f_builtins, f_globals, f_locals分别对应了三个PyDictObject,维护了builtin, global, 三个命名空间中name,以及local命名空间中name和value之间的对应关系。因为每个PyCodeBlock所需要的栈空间大小都是不一样的,所以PyFrameObject是一个可变长的对象,而f_localsplus所指向的就是PyCodeBlock所需要的栈空间和其他一些内存。实际上,运行时栈空间是f_valuestack和f_stacktop之间的空间,其他的空间是用来存放闭包变量的。Python标准库中的sys模块提供了对PyFrameObject的访问,具体的函数是sys._getframe()。
在Python中赋值语句会在local命名空间中创建约束(所谓的约束,就是变量名和变量名对应的值的对应关系)。赋值语句在Python中不单单指存在赋值符号的语句,还包括了def, class, import,for这样的语句。关于命名空间这个东西记住一点:Python中命名空间是静态的,而不是动态的。也就是说作用域仅仅由源程序的文本决定,而不是由运行时动态决定的(所以也叫词法作用域)。至于Python的LGB规则和LEGB规则我也不想多说了,只要按照最内嵌套作用域规则来推导就行了。关于Python的静态作用域,还有一个比较有意思的问题,之前我们老板面试我的时候问过我O(^_^)O
a = 1 def g(): print a def f(): print a a = 2 print a g() f()
当时我认为输出的结果是1 1 2。其实这段代码在运行f()第一个print a的时候就报错了,”local variable ‘a’ referenced before assignment”。简单的说,原因就是在编译成pyc文件的时候,f()中a的变量就已经确定是local变量了,所以在执行第一句print a的时候,发现a是未定义的,所以就报错了。通过dis查看编译生成的字节码就能看出两个print a的差异(g()中的print a和f()中的第一个print a的差异)。
g()中print a的字节码为 # 0 LOAD_GLOBAL 0 # 3 PRINT_ITEM # 4 PRINT_NEWLINE f()中第一句print a的字节码为 # 0 LOAD_FAST 0 # 3 PRINT_ITEM # 4 PRINT_NEWLINE
LOAD_GLOBAL的意思是从global空间中加载变量 而 LOAD_FAST是从local空间中加载变量。
在Python中Python解释器充当了CPU的角色,Python解释器中可能运行了很多进程,每个进程中可能有很多线程,而线程下面又有很多栈帧。这样一想整个Python虚拟机的框架就出来了。O(^_^)O