Python虚拟机中的一般表达式(三)
其他一般表达式
在前两章:Python虚拟机中的一般表达式(一)、Python虚拟机中的一般表达式(二)中,我们介绍了Python虚拟机是怎样执行创建一个整数值对象、字符串对象、字典对象和列表对象。现在,我们再来学习变量赋值、变量运算和print操作,Python是如何执行的
还是和以前一样,我们看一下normal.py对应的PyCodeObject所对应的符号表和常量
# cat normal.py a = 5 b = a c = a + b print(c) # python2.5 …… >>> source = open("normal.py").read() >>> co = compile(source, "normal.py", "exec") >>> co.co_names ('a', 'b', 'c') >>> co.co_consts (5, None)
以及normal.py所对应的字节码指令
>>> import dis >>> dis.dis(co) 1 0 LOAD_CONST 0 (5) 3 STORE_NAME 0 (a) 2 6 LOAD_NAME 0 (a) 9 STORE_NAME 1 (b) 3 12 LOAD_NAME 0 (a) 15 LOAD_NAME 1 (b) 18 BINARY_ADD 19 STORE_NAME 2 (c) 4 22 LOAD_NAME 2 (c) 25 PRINT_ITEM 26 PRINT_NEWLINE 27 LOAD_CONST 1 (None) 30 RETURN_VALUE >>>
第一行a = 5所对应的字节码指令这里不再叙述,我们开始看第二行的b = a
b = a //分析结果 2 6 LOAD_NAME 0 (a) 9 STORE_NAME 1 (b)
上面我们看到了一个新的指令:LOAD_NAME,这里,我们再看一下LOAD_NAME指令的内容,看看它到底做了什么,是如何配合STORE_NAME,完成赋值语句的
ceval.c
case LOAD_NAME: w = GETITEM(names, oparg); if ((v = f->f_locals) == NULL) { PyErr_Format(PyExc_SystemError, "no locals when loading %s", PyObject_REPR(w)); break; } if (PyDict_CheckExact(v)) { //[1]:在local名字空间中查找变量名对应的变量值 x = PyDict_GetItem(v, w); Py_XINCREF(x); } else { x = PyObject_GetItem(v, w); if (x == NULL && PyErr_Occurred()) { if (!PyErr_ExceptionMatches(PyExc_KeyError)) break; PyErr_Clear(); } } if (x == NULL) { //[2]:在global名字空间中查找变量名对应的变量值 x = PyDict_GetItem(f->f_globals, w); if (x == NULL) { //[3]:在builtin名字空间中查找变量名对应的变量值 x = PyDict_GetItem(f->f_builtins, w); if (x == NULL) { //[4]:查找变量名失败,抛出异常 format_exc_check_arg( PyExc_NameError, NAME_ERROR_MSG ,w); break; } } Py_INCREF(x); } PUSH(x); continue;
LOAD_NAME其实看上去内容很多,实际上很简单,结合上面代码的注释[1]、[2]、[3]处,我们可以知道,LOAD_NAME无非就是在local、global和builtin三个名字空间中查找一个变量名所对应的值,如果直到builtin名字空间也找不到,就抛出异常。而找到变量名对应的值之后,再压入运行时栈。最后配合STORE_NAME,完成赋值语句。
而Python的官方文档也描述了变量的搜索会沿着局部作用域(local名字空间)、全局作用域(global名字空间)和内建作用域(builtin名字空间)依次上溯,直至搜索成功或全部搜完3个作用域
让我们稍微修改一下LOAD_NAME的代码,然后重新编译安装Python
case LOAD_NAME: w = GETITEM(names, oparg); if ((v = f->f_locals) == NULL) { PyErr_Format(PyExc_SystemError, "no locals when loading %s", PyObject_REPR(w)); break; } if (PyDict_CheckExact(v)) { x = PyDict_GetItem(v, w); //[1] if(strcmp(PyString_AsString(w),"PythonVM")==0){ printf("[LOAD NAME]:Search PyObject %s in local name space...%s\n",PyString_AsString(w),x==NULL? "False": "Success"); } Py_XINCREF(x); } else { x = PyObject_GetItem(v, w); if (x == NULL && PyErr_Occurred()) { if (!PyErr_ExceptionMatches(PyExc_KeyError)) break; PyErr_Clear(); } } if (x == NULL) { x = PyDict_GetItem(f->f_globals, w); //[2] if(strcmp(PyString_AsString(w),"PythonVM")==0){ printf("[LOAD NAME]:Search PyObject %s in global name space...%s\n", PyString_AsString(w), x==NULL? "False": "Success"); } if (x == NULL) { x = PyDict_GetItem(f->f_builtins, w); //[3] if(strcmp(PyString_AsString(w),"PythonVM")==0){ printf("[LOAD NAME]:Search PyObject %s in builtin name space...%s\n", PyString_AsString(w), x==NULL? "False": "Success"); } if (x == NULL) { //[4] if(strcmp(PyString_AsString(w),"PythonVM")==0){ printf("[LOAD NAME]:Search PyObject %s faild\n", PyString_AsString(w)); } format_exc_check_arg( PyExc_NameError, NAME_ERROR_MSG ,w); break; } } Py_INCREF(x); } PUSH(x); continue;
我们在[1]、[2]、[3]、[4]处加入代码,尝试搜索符号的过程
# python2.5 …… >>> print PythonVM [LOAD NAME]:Search PyObject PythonVM in local name space...False [LOAD NAME]:Search PyObject PythonVM in global name space...False [LOAD NAME]:Search PyObject PythonVM in builtin name space...False [LOAD NAME]:Search PyObject PythonVM faild Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'PythonVM' is not defined
我们未曾定义PythonVM这个变量,在打印PythonVM这个符号时会先获取PythonVM对应的值,也就是会执行LOAD_NAME这条指令,可以看到,搜索变量时确实是按着local、global、builtin这三个名字空间搜索
数值运算
前面我们已经介绍了赋值操作所对应的字节码时如何执行的,这一小节,我们在基于之前的内容,了解数值运算
c = a + b //分析结果 3 12 LOAD_NAME 0 (a) 15 LOAD_NAME 1 (b) 18 BINARY_ADD 19 STORE_NAME 2 (c)
LOAD_NAME将a和b的值读取,并压入运行时栈,,然后通过BINARY_ADD进行加法运算,根据后面的STORE_NAME,可以猜测在BINARY_ADD中,已经把运算结果压入运行时栈,最后再STORE_NAME弹出其结果,建立符号和值的关系。现在,我们来看一下BINARY_ADD
case BINARY_ADD: w = POP(); v = TOP(); if (PyInt_CheckExact(v) && PyInt_CheckExact(w)) { //[1]:内置PyIntObject对象相加的快速通道 register long a, b, i; a = PyInt_AS_LONG(v); b = PyInt_AS_LONG(w); i = a + b; //[2]:如果其结果溢出,转向慢速通道 if ((i^a) < 0 && (i^b) < 0) { goto slow_add; } x = PyInt_FromLong(i); } //[3]:PyStringObject对象相加的快速通道 else if (PyString_CheckExact(v) && PyString_CheckExact(w)) { x = string_concatenate(v, w, f, next_instr); /* string_concatenate consumed the ref to v */ goto skip_decref_vx; } else { //[4]:一般对象相加的慢速通道 slow_add: x = PyNumber_Add(v, w); } Py_DECREF(v); skip_decref_vx: Py_DECREF(w); SET_TOP(x); if (x != NULL) continue; break;
从上面的代码[1]、[2]、[3]、[4]处可以看到,如果对象是int对象,则将其值取出,然后相加再检测是否溢出,如果溢出则走对象相加的慢速通道,如果没有溢出则返回,如果是PyStringObject对象相加,则根据相加结果创建新的PyStringObject对象返回
如果参与运算的对象是这两种快速通道之外的情况,那只能走慢速通道PyNumber_Add完成加法运算。在PyNumber_Add中,Python虚拟机会进行大量的类型判断,寻找与对象相对应的加法操作函数等额外工作,速度会比前两种加速机制慢上很多。一般来说,Python虚拟机在PyNumber_Add中首先检查参与运算的对象的类型对象,检查PyNumberMethods中的nb_add能否完成v和w上的加法运算,如果不能,还会检查PySequenceMethods中的sq_concat能否完成,如果都不能,则会抛出异常
这里需要注意一点的是,虽然Python虚拟机为PyIntObject对象准备了快速通道,但是如果计算结果溢出,Python虚拟机会放弃快速通道的计算结果,转向慢速通道。为了验证之前所说,我们再次修改BINARY_ADD的代码,在[1]、[2]、[3]处加上监测代码:
case BINARY_ADD: w = POP(); v = TOP(); if (PyInt_CheckExact(v) && PyInt_CheckExact(w)) { /* INLINE: int + int */ register long a, b, i; a = PyInt_AS_LONG(v); b = PyInt_AS_LONG(w); i = a + b; if ((i ^ a) < 0 && (i ^ b) < 0) { //[1] printf("[BINARY_ADD]:%ld + %ld in quick channel...overflow\n", a, b); goto slow_add; } //[2] printf("[BINARY_ADD]:%ld + %ld in quick channel...success\n", a, b); x = PyInt_FromLong(i); } else if (PyString_CheckExact(v) && PyString_CheckExact(w)) { x = string_concatenate(v, w, f, next_instr); /* string_concatenate consumed the ref to v */ goto skip_decref_vx; } else { slow_add: //[3] if (PyInt_CheckExact(v) && PyInt_CheckExact(w)) { register long a, b; a = PyInt_AS_LONG(v); b = PyInt_AS_LONG(w); printf("[BINARY_ADD]:%ld + %ld switch to slow channel\n", a, b); } x = PyNumber_Add(v, w); } Py_DECREF(v); skip_decref_vx: Py_DECREF(w); SET_TOP(x); if (x != NULL) continue; break;
然后编译安装Python,测试一下BINARY_ADD的行为:
# python2.5 …… >>> a = 1 >>> b = 2 >>> a + b [BINARY_ADD]:1 + 2 in quick channel...success 3 >>> c = 9223372036854775807 >>> d = c + c [BINARY_ADD]:9223372036854775807 + 9223372036854775807 in quick channel...overflow [BINARY_ADD]:9223372036854775807 + 9223372036854775807 switch to slow channel >>> type(d) <type 'long'>
信息输出
最后来看一下print的动作,在前面的normal.py中,最后我们打印了c这个对象,我们来看一下对应的字节码:
print(c) //分析结果 4 22 LOAD_NAME 2 (c) 25 PRINT_ITEM 26 PRINT_NEWLINE
在打印对象之前,一定要获取它的值,所以第一条字节码指令是LOAD_NAME,将c的值从名字空间取出,然后压入运行时栈,最后通过PRINT_ITEM完成打印操作
case PRINT_ITEM: v = POP(); if (stream == NULL || stream == Py_None) { w = PySys_GetObject("stdout"); if (w == NULL) { PyErr_SetString(PyExc_RuntimeError, "lost sys.stdout"); err = -1; } } Py_XINCREF(w); if (w != NULL && PyFile_SoftSpace(w, 0)) err = PyFile_WriteString(" ", w); if (err == 0) err = PyFile_WriteObject(v, w, Py_PRINT_RAW); ………//省略部分代价 Py_XDECREF(w); Py_DECREF(v); Py_XDECREF(stream); stream = NULL; if (err == 0) continue; break;
Python在打印时会判断一个名为stream的对象是否为NULL,如果为NULL则将w设置为标准输出流。那么,stream是什么呢?它实际上是定义在PyEval_EvalFrameEx中的一个PyObject对象
register PyObject *stream = NULL;
如果输出的时候,是通过如下的Python代码:
# cat demo3.py f = open("test", "w") print >> f, 1 # python2.5 …… >>> source = open("demo3.py").read() >>> co = compile(source, "demo3.py", "exec") >>> import dis >>> dis.dis(co) 1 0 LOAD_NAME 0 (open) 3 LOAD_CONST 0 ('test') 6 LOAD_CONST 1 ('w') 9 CALL_FUNCTION 2 12 STORE_NAME 1 (f) 2 15 LOAD_NAME 1 (f) 18 DUP_TOP 19 LOAD_CONST 2 (1) 22 ROT_TWO 23 PRINT_ITEM_TO 24 PRINT_NEWLINE_TO 25 LOAD_CONST 3 (None) 28 RETURN_VALUE >>>
那么在执行PRINT_ITEM之前,将会执行PRINT_NEWLINE_TO这条指令
case PRINT_ITEM_TO: w = stream = POP(); /* fall through to PRINT_ITEM */ case PRINT_ITEM: ……
可以看到,在执行PRINT_NEWLINE_TO时就给stream赋值了,同时也赋值给w。所以实际上stream是作为一个判断条件来使用的,真正使用的输出目标是w。要多次使用这一个stream的原因是变量w在别的字节码指令中可能还会用到,所以无法通过判断w是否为NULL来确定是否需要输出到标准输出流,可以看到,在PRINT_ITEM最后,又将stream设置为NULL,为下次输出时的判断做准备
在获得输出的目标和待输出的对象后,PRINT_ITEM将通过PyFile_WriteObject->PyObject_Print->internal_print的调用序列最终调用v->ob_type->tp_print等待输出对象自身所携带的输出函数进行输出。如果对象没有定义tp_print,它就会调用tp_str或tp_repr获得对象的字符串表示形式,然后将字符串输出