zoukankan      html  css  js  c++  java
  • 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
    ",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
    ", 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
    ", 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
    ", 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
    ", a, b);
    			goto slow_add;
    		}
    		//[2]
    		printf("[BINARY_ADD]:%ld + %ld in quick channel...success
    ", 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
    ", 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获得对象的字符串表示形式,然后将字符串输出

  • 相关阅读:
    Python基础04 字典基本操作
    Python基础03 列表、元组基本操作
    Python基础02 字符串基本操作
    Python基础07 函数作用域、嵌套函数、闭包函数、高阶函数及装饰器的理解
    Python随机数random模块学习,并实现生成6位验证码
    Python与时间相关的time、datetime模块的使用
    Python PIL库安装
    Python中可变对象和不可变对象
    Mac环境下Docker及Splash的安装运行教程
    redis 链表(list)操作
  • 原文地址:https://www.cnblogs.com/beiluowuzheng/p/9463130.html
Copyright © 2011-2022 走看看