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获得对象的字符串表示形式,然后将字符串输出

 

posted @ 2018-08-12 18:15  北洛  阅读(612)  评论(0编辑  收藏  举报