10.0 序
上一章中,我们通过PyEval_EvalFrameEx看到了python虚拟机的整体框架,那么这一章我们将深入到PyEval_EvalFrameEx的各个细节当中,深入剖析python的虚拟机,在本章中我们将剖析python虚拟机是如何完成对一般表达式的执行的。这里的一般表达式包括最基本的对象创建语句、打印语句等等。至于if、while等表达式,我们将其归类于控制流语句,对于python中控制流的剖析,我们将留到下一章。
10.1 简单内建对象的创建
# a.py
i = 1
s = "py"
d = {}
l = []
我们在a.py的foo函数中创建了几个简单的对象,我们分析一下其字节码的含义。不过在此之前我们需要看一些宏,这是PyFrame_EvalFrameEx在遍历指令序列co_code时所需要的宏,里面包括了对栈的各种操作,以及对tuple元素的访问操作。
/* Tuple access macros */
//获取tuple中的元素
#ifndef Py_DEBUG
#define GETITEM(v, i) PyTuple_GET_ITEM((PyTupleObject *)(v), (i))
#else
#define GETITEM(v, i) PyTuple_GetItem((v), (i))
#endif
//调整栈顶指针
#define BASIC_STACKADJ(n) (stack_pointer += n)
#define STACKADJ(n) { (void)(BASIC_STACKADJ(n), \\
lltrace && prtrace(TOP(), "stackadj")); \\
assert(STACK_LEVEL() <= co->co_stacksize); }
//入栈操作
#define BASIC_PUSH(v) (*stack_pointer++ = (v))
#define PUSH(v) BASIC_PUSH(v)
//出栈操作
#define BASIC_POP() (*--stack_pointer)
#define POP() ((void)(lltrace && prtrace(TOP(), "pop")), \\
BASIC_POP())
我们来看一下这个a.py的字节码,使用dis模块来分析一下
2 0 LOAD_CONST 0 (1)
2 STORE_NAME 0 (i)
3 4 LOAD_CONST 2 (\'py\')
6 STORE_NAME 1 (s)
4 8 BUILD_MAP 0
10 STORE_NAME 2 (d)
5 12 BUILD_LIST 0
14 STORE_NAME 3 (l)
16 LOAD_CONST 0 (None)
18 RETURN_VALUE
第一列显然表示行号,对应源文件的2、3、4、5行
我们来看看字节码指令如何影响当前活动的PyFrameObject对象中的运行时栈和local命名空间(f->f_locals)
字节码指令对符号或者常量的操作最终都会反应到运行时栈和local命名空间中
我们来看一下运行时栈和local空间的初始情况,在local空间中将存储执行过程中的局部变量,同时它也是python虚拟机的局部变量表,说白了就是一个PyDictObject对象,存储一个个变量名:变量值
的键值对。
我们下面来仔细分析一下字节码
2 0 LOAD_CONST 0 (1)
2 STORE_NAME 0 (i)
我们发现源代码为2,对应了两条字节码,说明赋值这个动作实际上是两个指令
TARGET(LOAD_CONST) {
PyObject *value = GETITEM(consts, oparg);
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH();
}
GETITEM(consts, oparg)显然是GETITEM(consts, 0),就是从consts中读取索引为0的这个元素
然后将其压入虚拟机的运行时栈当中,其实这个consts就是f->f_code->co_consts
其中f是当前活动的PyFrameObject对象,那么其实consts也就是PyCodeObject里面的co_consts
这里面只包含了常量值,因此我们也会把consts称之为常量表
这是第一行字节码,显然在执行完毕之后只改变了运行时栈,并没有改变local。
但是不用想也知道,执行i = 1这个操作应该在local命名空间中创建一个从符号i到PyLongObject对象1之间的映射,这样我们后面才能通过符号i找到其对应的对象
python虚拟机通过执行字节码指令STORE_NAME来改变local命名空间,从而完成变量名i到变量值1之间的映射
TARGET(STORE_NAME) {
//从符号表中获取符号,显然oparg=0
PyObject *name = GETITEM(names, oparg);
//从运行时栈中获取值,
PyObject *v = POP();
//拿到f_locals
PyObject *ns = f->f_locals;
int err;
//类型检测
if (ns == NULL) {
PyErr_Format(PyExc_SystemError,
"no locals found when storing %R", name);
Py_DECREF(v);
goto error;
}
//将符号、值的映射关系存储到local命名空间中
if (PyDict_CheckExact(ns))
err = PyDict_SetItem(ns, name, v);
else
err = PyObject_SetItem(ns, name, v);
Py_DECREF(v);
if (err != 0)
goto error;
DISPATCH();
}
现在我们发现变量名和变量值在内存中是通过一种怎样的关系捆绑在一起的了,注意:由于我们从运行时栈中获取值执行的是pop操作,所以此时运行栈中不存在任何对象了
然后第二行代码存储字符串和第一行是一样的,只不过参数不同罢了,此时如下:
但是在源代码的第四行,我们看到了有意思的东西
4 8 BUILD_MAP 0
10 STORE_NAME 2 (d)
字节码偏移8这里不再是load了,因为和int、str不同,不会直接load,虚拟机在执行该字节码指令时直接创建了一个PyDictObject对象,然后压入运行时栈中
TARGET(BUILD_MAP) {
Py_ssize_t i;
PyObject *map = _PyDict_NewPresized((Py_ssize_t)oparg);
if (map == NULL)
goto error;
for (i = oparg; i > 0; i--) {
int err;
PyObject *key = PEEK(2*i);
PyObject *value = PEEK(2*i - 1);
err = PyDict_SetItem(map, key, value);
if (err != 0) {
Py_DECREF(map);
goto error;
}
}
while (oparg--) {
Py_DECREF(POP());
Py_DECREF(POP());
}
PUSH(map);
DISPATCH();
}
然后执行STORE_NAME
然而对于最后一行代码,居然出现了4条字节码。
5 12 BUILD_LIST 0
14 STORE_NAME 3 (l)
16 LOAD_CONST 0 (None)
18 RETURN_VALUE
对于BUILD_LIST和BUILD_MAP类似
TARGET(BUILD_LIST) {
PyObject *list = PyList_New(oparg);
if (list == NULL)
goto error;
while (--oparg >= 0) {
PyObject *item = POP();
PyList_SET_ITEM(list, oparg, item);
}
PUSH(list);
DISPATCH();
}
可以推测,如果源代码创建的不是一个空的list,那么在BUILD_LIST指令前,一定会有许多LOAD_CONST操作,这将导致有许多对象被压入运行时栈中,在真正执行BUILD_LIST时,再将这些对象从栈里面一一弹出来,加入到新创建的PyListObject对象当中,因为这些对象其实就是list中的元素。
然后在执行完STORE_NAME,将变量名l和值映射之后,按理说就应该结束了,但是为什么还多了两行呢?
其实python在执行完一段代码块的时候,一定要范围一些,这两个字节码就是用来返回某些值的。可以看到LOAD_CONST将None给load进来,然后RETURN_VALUE返回回去。
TARGET(RETURN_VALUE) {
retval = POP();
why = WHY_RETURN;
goto fast_block_end;
}
在所有字节码指令都执行完毕之后,运行时栈就变空了,但所有信息都存储到了local命名空间中。
10.2 复杂内建对象的创建
我们前面看到了,python创建空的dict、空的list的过程。那么如果创建非空的dict和list,python运行时行为又是怎么样的呢?
#a.py
i = 1
s = "python"
d = {"1": 1, "2": 2}
l = [1, 2]
显然对于符号表(names, f->f_code->co_names)来说,在运行期间和之间应该是一样的,而常量表(consts,f->f_code->co_consts)来说则是不同的。
2 0 LOAD_CONST 1 (1)
2 STORE_NAME 0 (i)
3 4 LOAD_CONST 2 (\'python\')
6 STORE_NAME 1 (s)
4 8 LOAD_CONST 1 (1)
10 LOAD_CONST 3 (2)
12 LOAD_CONST 4 ((\'1\', \'2\'))
14 BUILD_CONST_KEY_MAP 2
16 STORE_NAME 2 (d)
5 18 LOAD_CONST 1 (1)
20 LOAD_CONST 3 (2)
22 BUILD_LIST 2
24 STORE_NAME 3 (l)
26 LOAD_CONST 0 (None)
28 RETURN_VALUE
首先源代码的第2、3行和之前一样,我们看源代码的第4行,我们看到了3个LOAD_CONST,表示将两个值和所有的keyload进来,然后BUILD_CONST_KEY_MAP比较重要,后面有一个2,这个2表示的是要创建的字典里面有两个元素
TARGET(BUILD_CONST_KEY_MAP) {
Py_ssize_t i;
PyObject *map;
PyObject *keys = TOP();
if (!PyTuple_CheckExact(keys) ||
PyTuple_GET_SIZE(keys) != (Py_ssize_t)oparg) {
PyErr_SetString(PyExc_SystemError,
"bad BUILD_CONST_KEY_MAP keys argument");
goto error;
}
map = _PyDict_NewPresized((Py_ssize_t)oparg);
if (map == NULL) {
goto error;
}
for (i = oparg; i > 0; i--) {
int err;
PyObject *key = PyTuple_GET_ITEM(keys, oparg - i);
PyObject *value = PEEK(i + 1);
err = PyDict_SetItem(map, key, value);
if (err != 0) {
Py_DECREF(map);
goto error;
}
}
Py_DECREF(POP());
while (oparg--) {
Py_DECREF(POP());
}
PUSH(map);
DISPATCH();
}
会根据load进来的values和keys创建PyDictObject对象,然后STORE_NAME,这样一个dict对象就创建完成了。
同理对于list的创建就更简单了,同样是先load每一个元素,并压入运行时栈,然后BUILD_LIST,创建完PyListObject对象,再从运行时栈中依次将对象取出,塞入PyListObject对象维护的"列表"中。最后STORE_NAME。然后load None、返回
10.2.1 函数中的变量
我们之前的变量是在模块级别的作用域中,但如果我们在函数中定义呢?
def foo():
i = 1
s = "python"
2 0 LOAD_CONST 1 (1)
2 STORE_FAST 0 (i)
3 4 LOAD_CONST 2 (\'python\')
6 STORE_FAST 1 (s)
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
我们看到大致一样,但是有一点发生了变化, 那就是在将变量名和变量值映射的时候,使用的不再是STORE_NAME,而是STORE_FAST,显然STORE_FAST会更快一些。为什么这么做,这是因为函数中的局部变量总是固定不变的,在编译的时候就能确定局部变量使用的内存空间的位置,也能确定局部变量字节码指令应该如何访问内存,就能使用静态的方法来实现局部变量。其实局部变量的读写都在fastlocals = f->f_localsplus上面
TARGET(STORE_FAST) {
PyObject *value = POP();
SETLOCAL(oparg, value);
FAST_DISPATCH();
}
#define SETLOCAL(i, value) do { PyObject *tmp = GETLOCAL(i); \\
GETLOCAL(i) = value; \\
Py_XDECREF(tmp); } while (0)
#define GETLOCAL(i) (fastlocals[i])
10.3 一般表达式
符号搜索
a = 5
b = a
c = a + b
print(c)
还是a.py,里面写了一些简单的语句,我们来看看它的字节码如何。
import dis
f = open("a.py", "r", encoding="utf8").read()
dis.dis(f)
"""
1 0 LOAD_CONST 0 (5)
2 STORE_NAME 0 (a)
2 4 LOAD_NAME 0 (a)
6 STORE_NAME 1 (b)
3 8 LOAD_NAME 0 (a)
10 LOAD_NAME 1 (b)
12 BINARY_ADD
14 STORE_NAME 2 (c)
4 16 LOAD_NAME 3 (print)
18 LOAD_NAME 2 (c)
20 CALL_FUNCTION 1
22 POP_TOP
24 LOAD_CONST 1 (None)
26 RETURN_VALUE
"""
首先源代码第一行无需介绍,但是第二行我们发现,不再是LOAD_CONST,而是LOAD_NAME,其实也很好理解。第一行a = 5, 而5是一个常量所以是LOAD_CONST,但是b = a,这里的a是一个变量名,所以是LOAD_NAME。
//这里显然要从几个命名空间里面去寻找指定的变量名对应的值
//找不到就会出现NameError
TARGET(LOAD_NAME) {
//从符号表里面获取变量名
PyObject *name = GETITEM(names, oparg);
//获取local命名空间的里面键值对
PyObject *locals = f->f_locals;
PyObject *v;
if (locals == NULL) {
PyErr_Format(PyExc_SystemError,
"no locals when loading %R", name);
goto error;
}
//根据变量名从locals里面获取对应的value
if (PyDict_CheckExact(locals)) {
v = PyDict_GetItem(locals, name);
Py_XINCREF(v);
}
else {
v = PyObject_GetItem(locals, name);
if (v == NULL) {
if (!PyErr_ExceptionMatches(PyExc_KeyError))
goto error;
PyErr_Clear();
}
}
//如果v是NULL,说明local命名空间里面没有
if (v == NULL) {
//于是从global命名空间里面找
v = PyDict_GetItem(f->f_globals, name);
Py_XINCREF(v);
//如果v是NULL说明global里面也没有
if (v == NULL) {
//下面的if和else里面的逻辑基本一致,只不过对builtin做了检测
if (PyDict_CheckExact(f->f_builtins)) {
//local、global都没有,于是从builtin里面找
v = PyDict_GetItem(f->f_builtins, name);
//还没有,NameError
if (v == NULL) {
format_exc_check_arg(
PyExc_NameError,
NAME_ERROR_MSG, name);
goto error;
}
Py_INCREF(v);
}
else {
//从builtin里面找
v = PyObject_GetItem(f->f_builtins, name);
if (v == NULL) {
//还没有,NameError
if (PyErr_ExceptionMatches(PyExc_KeyError))
format_exc_check_arg(
PyExc_NameError,
NAME_ERROR_MSG, name);
goto error;
}
}
}
}
//找到了,把v给push进去,相当于压栈
PUSH(v);
DISPATCH();
}
另外如果是在函数里面,这里b = a,那么就既不是LOAD_CONST、也不是LOAD_NAME,而是LOAD_FAST。这是因为函数中的变量在编译的时候就已经确定,因此是LOAD_FAST。那么如果a=5定义在函数外面呢?那么结果是LOAD_GLOBAL,因为知道这个a到底是定义在什么地方。
数值运算
再来看看c=a+b的字节码
3 8 LOAD_NAME 0 (a)
10 LOAD_NAME 1 (b)
12 BINARY_ADD
14 STORE_NAME 2 (c)
插一嘴,我们看到5这个数值只被load了一次,当b=a的时候,只是将b指向了a,说明a和b都指向了同一个PyLongObject对象。那么它们是如何结合的呢?
python虚拟机首先会通过两条LOAD_NAME指令将变量名a和b所对应的变量值从local命名空间读取出来,压入运行时栈,然后通过BINARY_ADD进行加法运算,计算两个变量的和。假设计算之后的结果为sum,那么python在获得结果sum之后,会通过STORE_NAME将(\'c\', sum)插入到local命名空间。当然这里只是加法,当然减法、乘法等等也是类似的。我们再来看看那个BINARY_ADD
TARGET(BINARY_ADD) {
//获取两个值,也就是我们a和b对应的值
PyObject *right = POP();
PyObject *left = TOP();
PyObject *sum;
//这里检测是否是字符串
if (PyUnicode_CheckExact(left) &&
PyUnicode_CheckExact(right)) {
//是的话直接拼接
sum = unicode_concatenate(left, right, f, next_instr);
}
else {
//不是的话相加
sum = PyNumber_Add(left, right);
Py_DECREF(left);
}
Py_DECREF(right);
//设置sum
SET_TOP(sum);
if (sum == NULL)
goto error;
DISPATCH();
}
信息输出
最后看看信息是如何输出的
4 16 LOAD_NAME 3 (print)
18 LOAD_NAME 2 (c)
20 CALL_FUNCTION 1
22 POP_TOP
24 LOAD_CONST 1 (None)
26 RETURN_VALUE
由于我们print(c),显然需要把print和c压入栈中。这里的print也是LOAD_NAME,因为我们可以自己也定义一个变量叫做print,如果我们没有定义一个叫做print的变量,那么得到的就是python里面用于打印的print。
CALL_FUNCTION,表示函数调用,执行刚才的print,后面的1则是参数的个数。另外,当调用print的时候,实际上又创建了一个栈帧,因为只要是函数调用都会创建栈帧的。
TARGET(CALL_FUNCTION) {
PyObject **sp, *res;
sp = stack_pointer;
res = call_function(&sp, oparg, NULL);
stack_pointer = sp;
PUSH(res);
if (res == NULL) {
goto error;
}
DISPATCH();
}
然后POP_TOP表示从栈的顶端把元素打印出来,这里显然是c的值。最后LOAD_CONST、RETURN_VALUE,无需解释了。
最后再来看看print是如何打印的
//Objects/bltinmodule.c
static PyObject *
builtin_print(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
{
//python里面print支持的参数
static const char * const _keywords[] = {"sep", "end", "file", "flush", 0};
static struct _PyArg_Parser _parser = {"|OOOO:print", _keywords, 0};
//初始化全部为NULL
PyObject *sep = NULL, *end = NULL, *file = NULL, *flush = NULL;
int i, err;
if (kwnames != NULL &&
!_PyArg_ParseStackAndKeywords(args + nargs, 0, kwnames, &_parser,
&sep, &end, &file, &flush)) {
return NULL;
}
if (file == NULL || file == Py_None) {
file = _PySys_GetObjectId(&PyId_stdout);
//默认输出到sys.stdout也就是控制台
if (file == NULL) {
PyErr_SetString(PyExc_RuntimeError, "lost sys.stdout");
return NULL;
}
/* sys.stdout may be None when FILE* stdout isn\'t connected */
if (file == Py_None)
Py_RETURN_NONE;
}
if (sep == Py_None) {
sep = NULL;
}
else if (sep && !PyUnicode_Check(sep)) {
PyErr_Format(PyExc_TypeError,
"sep must be None or a string, not %.200s",
sep->ob_type->tp_name);
return NULL;
}
if (end == Py_None) {
end = NULL;
}
else if (end && !PyUnicode_Check(end)) {
PyErr_Format(PyExc_TypeError,
"end must be None or a string, not %.200s",
end->ob_type->tp_name);
return NULL;
}
for (i = 0; i < nargs; i++) {
if (i > 0) {
if (sep == NULL)
//设置sep为空格
err = PyFile_WriteString(" ", file);
else
//否则说明用户了sep
err = PyFile_WriteObject(sep, file,
Py_PRINT_RAW);
if (err)
return NULL;
}
err = PyFile_WriteObject(args[i], file, Py_PRINT_RAW);
if (err)
return NULL;
}
//end同理,不指定的话默认是打印换行
if (end == NULL)
err = PyFile_WriteString("\\n", file);
else
err = PyFile_WriteObject(end, file, Py_PRINT_RAW);
if (err)
return NULL;
//flush则是否强制刷新控制台
if (flush != NULL) {
PyObject *tmp;
int do_flush = PyObject_IsTrue(flush);
if (do_flush == -1)
return NULL;
else if (do_flush) {
tmp = _PyObject_CallMethodId(file, &PyId_flush, NULL);
if (tmp == NULL)
return NULL;
else
Py_DECREF(tmp);
}
}
Py_RETURN_NONE;
}
思考题
为什么在python2中,while 1:比while True:要快,用上面的知识很容易完美解答,首先这里提示一下:python2中的True不是一个关键字。
答:因为在python2中True不是一个关键字,这就意味着我们可以使用True作为一个变量名,因此python会检测这个True这个名字有没有人用,如果local、global命名空间里面都没有的话,那么最后再去builtin里面拿到表示bool的True,显然每一次循环都要进行这样的检测。但对于while 1就不一样的,1是一个整数、而且还是小整数对象池里面的整数,这就说明直接LOAD_CONST即可,因为不可能拿一个数字当变量名,数字就是一个常量,没有True那些检测、查询的过程,所以while 1:比while True:要快。但是在python3中,这两者是一样的,因为True已经是一个关键字了,全局唯一,并且它是继承自int的,所以也是LOAD_CONST。