python虚拟机中的一般表达式

一、简单内建对象的创建

通常python字节码执行在虚拟机的一个栈帧中,执行的效果会影响当前环境运行时栈以及局部变量表,前者是栈帧的valuestack指针指向的一段动态内存,后者是栈帧的f_locals指向的一个字典对象。先讨论一个简单赋值语句i = 1。这个语句编译出的字节码通常如下:

1
2
3
4
# example 1
i = 1
0 LOAD_CONST 0 (1)
3 STORE_NAME 0 (i)

LOAD_CONST的效果是从代码对象的常量表中读取指定的值,这里显示读取第0个常量得到了1,第二条指令STORE_NAME完成的工作是将代码对象的名字表中读取第0个名字,并将其与当前栈顶元素绑定,存储到栈帧的局部名字空间中。也就是说第一个指令会将1压栈,但是不会影响名字空间,第二个指令将1出栈并影响局部名字空间,完成变量的名字与值的绑定。

再来看一个简单的赋值语句:d = {}

1
2
3
4
5
6
7
8
9
10
11
12
13
# example 2
d = {'a':1}
6 LOAD_CONST 1 ('a')
9 LOAD_CONST 0 (1)
12 BUILD_MAP 1
15 STORE_NAME 1 (d)

l = [1]
18 LOAD_CONST 0 (1)
21 BUILD_LIST 1
24 STORE_NAME 2 (l)
27 LOAD_CONST 2 (none)
30 RETURN_VALUE

掌握了例子1后,这个例子的理解就水到渠成了。BUILD_MAP没有读取常量表,而是直接新建了一个map对象压栈,这里的参数是告诉这个指令,当前栈顶有几需要读取的键值对,因为我们事先压栈了需要建立map的元素。然后STORE_NAME做的工作与前面一样,出栈、读取变量名并修改局部名字空间。然后对于l = []的编译稍微有一点不一样的东西了。这里的BUILD_LIST的参数是有用的,这个参数同样是告诉我们当前栈顶有多少元素需要添加到列表中,结合我们预先的LOAD_CONST可以得知,列表的构建过程会先将所需元素先行压栈然后再统一创建。索引为21的LOAD_CONST与后面的RETURN_VALUE的目的是为了返回一个空值,原因在于python的每一个code block都要有返回值。

二、名字搜索

变量名的查找是理解作用域以及名字空间的重中之重。前面的例子都是读取的常量或者直接创建一个新的对象,为了探究名字搜索的过程,看这么一个源码文件。

1
2
3
4
a = 5
b = a
c = a + b
print(c)

这个代码文件编译出的字节码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
a = 5
1 0 LOAD_CONST 0 (5)
3 STORE_NAME 0 (a)
b = a
2 6 LOAD_NAME 0 (a)
9 STORE_NAME 1 (b)
c = a+b
3 12 LOAD_NAME 0 (a)
15 LOAD_NAME 1 (b)
18 BINARY_ADD
19 STORE_NAME 2 (c)
print(c)
4 22 LOAD_NAME 3 (print)
25 LOAD_NAME 2 (c)
28 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
31 POP_TOP
32 LOAD_CONST 1 (None)
35 RETURN_VALUE

来看一下编译出的内容。前两个指令不用看了,已经了解了,这两条指令过后在当前栈帧的局部名字空间新增了绑定。b=a的指令出现了LOAD_NAME,这个指令需要从名字空间中所搜名字,他是怎么做的呢?这个LOAD_NAME要做的事情远比STORE_NAME复杂,因为后者只涉及局部名字空间,而前者涉及到回溯作用域链。下面展示了LOAD_NAME的C代码(已经删去大量的错误处理和类型检查的代码),可以发现LOAD_NAME中应用的是明显的LGB规则,那么LEGB规则是怎么来的呢?这个问题你可以回答吗?后面的文章中会对此作出解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* ---LOAD_NAME--- */
PyObject *name = GETITEM(names, oparg);
PyObject *locals = f->f_locals;
PyObject *v;

v = PyDict_GetItem(locals, name); // 搜索local
Py_XINCREF(v);

if (v == NULL) {
v = PyDict_GetItem(f->f_globals, name); // 搜索global
Py_XINCREF(v);
if (v == NULL) {
v = PyDict_GetItem(f->f_builtins, name); //搜索 buildin
if (v == NULL) { // LGB中都没有找到该名字
format_exc_check_arg(PyExc_NameError,
NAME_ERROR_MSG, name);
goto error;
}
Py_INCREF(v);

}
}

PUSH(v); // 压入运行时栈
DISPATCH();

由于写这个系列文章的目的在于深入理解python的函数机制,探究其中的作用域与名字空间的规则,后面两个涉及到的加法运算和打印操作在这里不进行详细解释,有兴趣的同学可以去翻看《python源码剖析》中的内容。


参考

  1. Python源码剖析(陈孺)
  2. Frame Hack
  3. Python解释器简介(5):深入主循环
本站总访问量