• Python虚拟机中的一般表达式(一)


    Python虚拟机框架这一章中,我们通过PyEval_EvalFrameEx看到了Python虚拟机的整体框架。而这章开始,我们将了解Python虚拟机是如何完成对Python的一般表达式的执行,这里的“一般表达式”包括最基本的对象创建语句,打印语句。至于if、while等表达式,我们将之归类于控制流语句,将再后面的章节介绍

    简单内建对象的创建

    我们先来看一段简单的对象创建语句:

    demo.py

    i = 1
    s = "Python"
    d = {}
    l = []
    

      

    上面的语句很简单,创建i、s、d、l四个变量,分别赋值为1、"Python"、字典、列表,现在,让我们看一下这个脚本所对应的PyCodeObject对象中的符号表co_names和常量表co_consts都包含了什么

    # python2.5
    ……
    >>> source = open("demo.py").read()
    >>> co = compile(source, "demo.py", "exec")
    >>> co.co_consts
    (1, 'Python', None)
    >>> co.co_names
    ('i', 's', 'd', 'l')
    

      

    符号表和常量表保存着程序运行的重要信息,在Python虚拟机执行字节码指令时具有非常重要的作用

    接下来,我们再用dis模块看一下demo.py对应的字节码

    >>> import dis
    >>> dis.dis(co)
      1           0 LOAD_CONST               0 (1)
                  3 STORE_NAME               0 (i)
    
      2           6 LOAD_CONST               1 ('Python')
                  9 STORE_NAME               1 (s)
    
      3          12 BUILD_MAP                0
                 15 STORE_NAME               2 (d)
    
      4          18 BUILD_LIST               0
                 21 STORE_NAME               3 (l)
                 24 LOAD_CONST               2 (None)
                 27 RETURN_VALUE  
    

      

    最左边的一列是字节码指令在源代码中所对应的行数,左起第二列是当前字节码在co_code中的偏移位置,第三列显示了当前字节码的指令,第四列是指令的参数,最后一列是计算后的实际参数

    在PyEval_EvalFrameEx的实现中,出于对效率的考虑,使用了大量的宏,其中的一些宏包括了对栈的各种操作以及对tupple元素的访问操作,在执行字节码指令时,会大量使用这些宏:

    ceval.c

    //访问tupple中的元素
    #define GETITEM(v, i) PyTuple_GetItem((v), (i))
    //调整栈顶指针
    #define BASIC_STACKADJ(n)	(stack_pointer += n)
    #define STACKADJ(n)	BASIC_STACKADJ(n)
    //入栈操作
    #define BASIC_PUSH(v)	(*stack_pointer++ = (v))
    #define PUSH(v)		BASIC_PUSH(v)
    //出栈操作
    #define BASIC_POP()	(*--stack_pointer)
    #define POP()		BASIC_POP()
    

      

    图1-1

    图1-1左边的stack_pointer是运行时栈的栈顶指针,字节码指令对符号和常量的操作最终都将反应到运行时栈和local名字空间(f->f_locals)

    我们对demo.py结合dis所分析的结果逐行解析

    i = 1
    //分析结果
    0 LOAD_CONST 0 (1)
    3 STORE_NAME 0 (i)
    

      

    i = 1产生了两条字节码:LOAD_CONST和STORE_NAME,我们现在看下LOAD_CONST在PyEval_EvalFrameEx函数中的定义:

    ceval.c

    case LOAD_CONST:
    	x = GETITEM(consts, oparg);
    	Py_INCREF(x);
    	PUSH(x);
    	goto fast_next_opcode;
    

      

    根据我们之前的定义,GETITEM(consts, oparg)显然就是GETITEM(consts, 0),即PyTuple_GetItem(consts, 0)。LOAD_CONST的意图很明显,就是从consts中读取序号为0的元素,然后再执行PUSH字节码将其压入运行时栈stack_pointer,其中,consts就是f->f_code->co_consts,其中,f是当前活动的PyFrameObject对象,那么consts也就是PyCodeObject对象中的co_consts

    根据dis模块对demo.py的解析,我们可以知道consts的第0个元素是一个整数对象1,这也是demo.py中所创建的第一个对象。LOAD_CONST完成后运行时栈和名字空间如下图所示:

    图1-2

    第一条字节码指令LOAD_CONST只改变了运行时栈,对local名字空间没有任何影响。但别忘了,完成i = 1除了LOAD_CONST这条指令,还有一条STORE_NAME指令,STORE_NAME将完成在local名字空间中,实现符号i到PyIntObject对象1之间的映射关系,这样以后如果我们需要符号i,就可以到local名字空间查找i所对应的对象。现在,我们再来看一下STORE_NAME字节码的实现

    ceva.lc

    case STORE_NAME:
    	w = GETITEM(names, oparg);
    	v = POP();
    	if ((x = f->f_locals) != NULL) {
    		if (PyDict_CheckExact(x))
    			err = PyDict_SetItem(x, w, v);
    		else
    			err = PyObject_SetItem(x, w, v);
    		Py_DECREF(v);
    		if (err == 0) continue;
    		break;
    	}
    	PyErr_Format(PyExc_SystemError,
    			 "no locals found when storing %s",
    			 PyObject_REPR(w));
    	break;
    

      

    这里,我们只考虑f->f_locals是PyDictObject对象的情况,代码会先取出符号表的符号,并从运行时栈中取出符号所对应的值,在PyDictObject这个对象中建立映射关系,而根据上面dis对i = 1的解析,可以发现STORE_NAME取出的符号确实是i。而完成STORE_NAME这一指令后,运行时栈和local名字空间的分部变为如下:

    图1-3

      

    而demo.py中的s = "Python"所对应的字节码与i = 1所对应的一模一样,这里不再做阐述。

    现在,我们再来看一下demo.py的第三行d = {},是如何创建一个字典对象

    d = {}
    //分析结果
    3 12 BUILD_MAP  0
      15 STORE_NAME 2 (d)
    

      

    指令BUILD_MAP会创建一个PyDictObject对象,并将之压入栈

    ceval.c

    case BUILD_MAP:
    	x = PyDict_New();
    	PUSH(x);
    	if (x != NULL) continue;
    	break;
    

      

    可能有人会想,如果在声明字典时不单单声明一个空字典,而是填入参数呢?如:d = {"1": 1, "2": 2},不要急,关于这样的字典对应的字节码是如何生成并执行的,后面还会再介绍,再执行完BUILD_MAP和STORE_NAME之后,我们再来看下运行时栈和名字空间:

    图1-4

     再来看一下demo.py最后一行代码l = [],我们看一下它的分析结果:

    l = []
    //分析结果
    4 18 BUILD_LIST    0
      21 STORE_NAME    3 (l)
      24 LOAD_CONST    2 (None)
      27 RETURN_VALUE  
    

      

    BUILD_LIST这个字节码比BUILD_MAP稍微好点,因为它不像BUILD_MAP那样创建一个空字典就直接压入栈,而是会根据列表中的元素生成一个列表

    ceval.c

    case BUILD_LIST:
    	x =  PyList_New(oparg);
    	if (x != NULL) {
    		for (; --oparg >= 0;) {
    			w = POP();
    			PyList_SET_ITEM(x, oparg, w);
    		}
    		PUSH(x);
    		continue;
    	}
    	break;
    

      

    这里我们可以做一个猜测,如果BUILD_LIST创建的不是一个空列表,那在之前一定有若干LOAD_CONST操作,这将导致若干元素压入运行时栈中,在执行BUILD_LIST时,这些元素又会从运行时栈中弹出,加入新创建的PyListObject对象中。最后,执行STORE_NAME,完成符号与栈中列表的映射。现在,运行时栈和名字空间的分布应该如下:

    图1-5

    到这里,似乎demo.py所有的代码都执行完毕,但似乎我们还漏了些什么?在创建列表并建立映射之后,还有两句字节码:

    24 LOAD_CONST    2 (None)
    27 RETURN_VALUE  
    

      

    既然对象都已经创建完毕,那么多出的这两句又有什么用呢?原来,Python在执行完一段Code Block之后,一定要返回一些值,这条字节码指令就是用来返回这些值的,LOAD_CONST将None这个对象压入运行时栈,再在RETURN_VALUE时将栈中的对象,也就是None返回

    ceval.c

    case RETURN_VALUE:
    	retval = POP();
    	why = WHY_RETURN;
    	goto fast_block_end;
    

      

  • 相关阅读:
    VC++ 之 文件操作
    Delphi7 API(5) 消息篇:WM_LBUTTONDOWN、WM_LBUTTONUP、WM_MOUSEMOVE
    VC++ 之 输入/输出类库(二)
    VB 访问控制面板
    Delphi7 API(4) 消息_重绘
    Lisp简明教程
    一次快速排序错误引发的思考(2)
    一次快速排序错误引发的思考(1)
    Common Lisp编译程序的小技巧
    暴风影音5免去广告的小技巧
  • 原文地址:https://www.cnblogs.com/beiluowuzheng/p/9461259.html
Copyright © 2020-2023  润新知