zoukankan      html  css  js  c++  java
  • 《python源代码剖析》笔记 python虚拟机中的函数机制

    本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie


    1.Python虚拟机在运行函数调用时会动态地创建新的 PyFrameObject对象,
    这些PyFrameObject对象之间会形成PyFrameObject对象链,模拟x86平台上执行时栈


    2.PyFuctionObject对象

    typedef struct {
    	PyObject_HEAD
    	PyObject *func_code; //相应函数编译后的PyCodeObject对象
    	PyObject *func_globals; //函数执行时的global空间
    	PyObject *func_defaults; //默认參数(tuple或NULL)
    	PyObject *func_closure; //NULL or a tuple of cell objects,用于实现closure
    	PyObject *func_doc; //函数的文档(PyStringObject)
    	PyObject *func_name; //函数名称,函数的__name__属性,(PyStringObject)
    	PyObject *func_dict; //函数的__dict__属性(PyDictObject或NULL)
    	PyObject *func_weakreflist;
    	PyObject *func_module; //函数的__module__,能够是不论什么对象
    } PyFunctionObject;

    函数的声明和函数的实现是分离在不同的PyCodeObject中的
    def f()创建了函数对象



    PYCodeObject:一个Code Block的静态信息。比方a = 1,符号a和值1以及它们的联系是一种静态信息,分别存储在
    PyCodeObject的常量表co_consts,符号表co_names以及字节码序列co_code中
    PyFunctionObject:包括函数的静态信息,由func_code指向函数代码相应的PyCodeObject对象,还包括了函数在运行时
    的动态信息,如func_globals。


    名字空间里键值对是动态信息,必须在执行时动态创建


    一段代码仅仅能相应一个PyCodeObject,却能够相应多个PyFunctionObject。


    3.无參函数调用
    ??图11-2,图11-3 为什么用这两个 strRef,internStr
    def f():
    #LOAD_CONST 0
    #MAKE_FUNCTION 0
    #STORE_NAME 0
    	print "Function"
    	#LOAD_CONST 1
    	#PRINT_ITEM
    	#PRINT_NEWLINE
    	#LOAD_CONST
    	#RETURN_VALUE
    f()
    #LOAD_NAME 0
    #CALL_FUNCTION 0
    #POP_TOP
    #LOAD_CONST 1
    #RETURN_VALUE


    
    

    def f()创建了函数对象,如图11-6所看到的 

     call_function函数调用

    static PyObject *
    call_function(PyObject ***pp_stack, int oparg
    #ifdef WITH_TSC
                    , uint64* pintr0, uint64* pintr1
    #endif
                    )
    {
    	//[1]:处理函数參数信息
        int na = oparg & 0xff; //位置參数、扩展位置參数个数
        int nk = (oparg>>8) & 0xff; //键參数、扩展键參数个数
        int n = na + 2 * nk; //总共的參数个数 nk*2是由于(键,值)对中键和值各占一个位置
        //[2]:获得PyFunctionObject对象
    	PyObject **pfunc = (*pp_stack) - n - 1; 
        PyObject *func = *pfunc; 
        PyObject *x, *w;
    
    
        if (PyCFunction_Check(func) && nk == 0) {
    		//...
    		
        } else {
            if (PyMethod_Check(func) && PyMethod_GET_SELF(func) != NULL) {
            
            } else
           //[3]:对PyFunctionObject对象进行调用
    	   if (PyFunction_Check(func))
                x = fast_function(func, pp_stack, n, na, nk);
            else
                x = do_call(func, pp_stack, na, nk);
            //...
        }
    	//...
        return x;
    }
    

    fast_function
    对于一般函数(除Python独有的函数(如Draw(x, *key, **dict))外的函数),PyEval_EvalFrameEx,进入一个新的PyFrameObject(栈桢)环境中開始运行新的字节码指令序列的循环
    还有一条路径,PyEval_EvalCodeEx
    static PyObject *
    fast_function(PyObject *func, PyObject ***pp_stack, int n, int na, int nk)
    {
        PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func);
        PyObject *globals = PyFunction_GET_GLOBALS(func);
        PyObject *argdefs = PyFunction_GET_DEFAULTS(func);
        PyObject **d = NULL;
        int nd = 0;
    
    
        //...
    	//[1]:一般函数的高速通道
        if (argdefs == NULL && co->co_argcount == n && nk==0 &&
            co->co_flags == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE)) {
            PyFrameObject *f;
            PyObject *retval = NULL;
            PyThreadState *tstate = PyThreadState_GET();
            PyObject **fastlocals, **stack;
            int i;
    
    
            f = PyFrame_New(tstate, co, globals, NULL);
            //...
    		retval = PyEval_EvalFrameEx(f,0);
            //...
    		return retval;
        }
        if (argdefs != NULL) {
            d = &PyTuple_GET_ITEM(argdefs, 0);
            nd = Py_SIZE(argdefs);
        }
        return PyEval_EvalCodeEx(co, globals,
                                 (PyObject *)NULL, (*pp_stack)-n, na,
                                 (*pp_stack)-2*nk, nk, d, nd,
                                 PyFunction_GET_CLOSURE(func));
    }

    4.函数运行时的名字空间
    LOAD_NAME会依次在f_locals, f_globals, f_builtins搜索符号
    在运行func_0.py的字节码指令序列时的global名字空间和运行函数f的字节码指令序列时的global名字空间实际上是同一个名字空间



    5.
    參数类型:位置參数、键參数、扩展位置參数、扩展键參数

              def f(  a,     n = "Python",       *list,               **key)
    扩展位置參数、扩展键參数用局部变量实现
    oparg:參数个数,仅仅需记录位置參数和键參数的个数,扩展位置參数和扩展键參数是特殊的位置參数和键參数
    oparg有两个字节,低字节-->位置參数的个数,高字节-->键參数的个数
    记录參数须要的内存数:n = na + 2*nk,由于位置參数仅仅需一条LOAD_CCONST,而键參数由于(键,值)对须要两条LOAD_CONST
    一个參数是位置參数还是键參数由实參决定,非键參数必须在键參数之前,eg.
    def fun(a, b):
    	pass
    fun(1, b = 2)





    5.位置參数的传递
    def f(name, age):
    #LOAD_CONST 0
    #MAKE_FUNCTION 0
    #STORE_NAME 0
    	age += 5
    	#LOAD_FAST 1
    	#LOAD_CONST 5
    	#INPLACE_ADD
    	#STORE_FAST 1
    	print "[", name, ",", age, "]"
    	#LOAD_CONST 2
    	#PRINT_ITEM
    	#LOAD_FAST 0
    	#PRINT_ITEM
    	#LOAD_CONST 3
    	#PRINT_ITEM
    	#LOAD_FAST 1
    	#PRINT_ITEM
    	#LOAD_CONST 4
    	#PRINT_ITEM
    	#PRINT_NEWLINE
    	#LOAD_CONST 0
    	#RETURN_VALUE
    age = 5;
    print age
    f("Robert", age)
    #LOAD_NAME 0
    #LOAD_CONST 2
    #LOAD_NAME 1
    #CALL_FUNCTION 2
    #POP_TOP
    print age
    call_function --> fast_function


    位置參数的訪问
    LOAD_FAST i, 将f_localsplus[i]中的对象到执行时栈中
    STORE_FAST i, 将执行时栈顶中的值存放到f_localsplus[i]中
    在调用函数时,Python将函数參数值从左到右到执行时栈中,在fast_function中,又将这些參数
    依次复制到新建的与函数相应的PyFrameObject对象的f_localsplus中,终于的效果就是,Python虚拟机将
    函数调用时传入的參数从左到右地依次存放在新建的PyFrameObject对象的f_localsplus中。
    在訪问函数參数时,Python虚拟机没有依照通常訪问符号的做法,去查什么名字空间,而是直接通过
    一个索引(偏移位置)来訪问f_localsplus中存储的符号相应的值对象。


    位置參数的默认值
    def f(a = 1, b = 2):
    #LOAD_CONST 0
    #LOAD_CONST 1
    #LOAD_CONST 2
    #MAKE_FUNCTION 2
    #STORE_NAME 0
    	print a + b
    	#LOAD_FAST 0
    	#LOAD_FAST 1
    	#BINARY_ADD
    	#PRINT_ITEM
    	#PRINT_NEWLINE
    	#LOAD_CONST 0
    	#RETURN_VALUE
    f()
    #LOAD_NAME 0
    #CALL_FUNCTION 0
    #POP_TOP
    f(b=3)
    #LOAD_NAME 0
    #LOAD_CONST 3
    #LOAD_CONST 4
    #CALL_FUNCTION 256
    #POP_TOP
    #LOAD_CONST 5
    #RETURN_VALUE

    不管函数有无參数,其def语句编译后的结果都是一样的,区别是在进行函数调用的时候产生的,
    无參函数在调用前公将PyFunctionObject对象压入执行时栈,而带參函数还需将參数也压入执行时栈
    而有默认參数值的函数的def语句编译后还会多出几条LOAD_CONST字节码,将默认值压入栈中,
    然后在MAKE_FUNCTION中,将这些默认值所有存放到一个PyTupleObject对象中,再将该对象设置为
    PyFrameObject.func_defaults的值。这样,函数參数的默认值也成为了PyFunctionObject对象的一部分。
    总结:PyFunctionObject对象包含三个基本的内容,PyCodeObject, globals名字空间和func_defaults

    PyObject *
    PyEval_EvalCodeEx(PyCodeObject *co, PyObject *globals, PyObject *locals,
               PyObject **args, int argcount, PyObject **kws, int kwcount,
               PyObject **defs, int defcount, PyObject *closure)
    {
        register PyFrameObject *f;
        register PyObject *retval = NULL;
        register PyObject **fastlocals, **freevars;
        PyThreadState *tstate = PyThreadState_GET();
        PyObject *x, *u;
    	//[1]:创建PyFrameObject对象
        f = PyFrame_New(tstate, co, globals, locals);
        
        fastlocals = f->f_localsplus;
        freevars = f->f_localsplus + co->co_nlocals;
    	
    	//[a]:遍历键參数,确定函数的def语句中是否出现了键參数的名字
    	for (i = 0; i < kwcount; i++) {
    		PyObject *keyword = kws[2*i];
    		PyObject *value = kws[2*i + 1];
    		int j;
    		//[b]:在函数的变量名表中寻找keyword
    		//...
    		for (j = 0; j < co->co_argcount; j++) {
    			PyObject *nm = co_varnames[j];
    			int cmp = PyObject_RichCompareBool(
    				keyword, nm, Py_EQ);
    			if (cmp > 0)
    				goto kw_found;
    			else if (cmp < 0)
    				goto fail;
    		}
    		if (kwdict == NULL) {
    			PyObject *kwd_str = kwd_as_string(keyword);
    			if (kwd_str) {
    				PyErr_Format(PyExc_TypeError,
    							 "%.200s() got an unexpected "
    							 "keyword argument '%.400s'",
    							 PyString_AsString(co->co_name),
    							 PyString_AsString(kwd_str));
    				Py_DECREF(kwd_str);
    			}
    			goto fail;
    		}
    		PyDict_SetItem(kwdict, keyword, value);
    		continue;
    	  kw_found:
    		if (GETLOCAL(j) != NULL) {
    			goto fail;
    		}
    		Py_INCREF(value);
    		SETLOCAL(j, value);
    	}
    	
        if (co->co_argcount > 0 ||
            co->co_flags & (CO_VARARGS | CO_VARKEYWORDS)) {
            int i;
            int n = argcount;
            PyObject *kwdict = NULL;
            //n为CALL_FUNCTION的參数指示的传入的位置參数个数,即na,这里为0
    		
    		//...
    		
    		//[2]:推断是否使用參数的默认值
    		if (argcount < co->co_argcount) {
                //m 一般位置參数的个数
    			int m = co->co_argcount - defcount;
    			//[3]:函数调用者必须传递一般位置參数的參数值
                for (i = argcount; i < m; i++) {
                    if (GETLOCAL(i) == NULL) {
                        goto fail;
                    }
                }
    			//[4]:n > m意味着调用者希望替换一些默认位置參数的默认值
                if (n > m)
                    i = n - m;
                else
                    i = 0;
    			//[5]:设置默认位置參数的默认值
                for (; i < defcount; i++) {
                    if (GETLOCAL(m+i) == NULL) {
                        PyObject *def = defs[i];
                        Py_INCREF(def);
                        SETLOCAL(m+i, def);
                    }
                }
            }
        }
        
        retval = PyEval_EvalFrameEx(f,0);
        return retval;
    }


    当终于须要设置默认值的參数个数确定之后,Python虚拟机会从PyFrameObject对象的func_defaults中将这些參数取出,
    并通过SETLOCAL将其放入PyFrameObject对象的f_localsplus所管理的内存块中。


    在编译时,Python会将函数的def语句中出现的參数的名称都记录在变量名表(co_varnames)中。


    对于第二次调用
    Python虚拟机会先在PyCodeObject对象中的co_varnames中查找'b',得到它相应的序号,然后通过SETLOCAL将
    新建的PyFrameObject对象中的f_localsplus中參数b相应的位置设置为3。接下来再为须要设置默认值的默认位置參数
    设置默认值。


    扩展位置參数和扩展键參数,是作为局部变量来实现的
    *list是由PyTupleObject实现,而**key是由PyDictObject对象实现
    在编译一个函数时,假设发现了*list这种扩展位置參数形式,会在PyCodeObject对象的co_flags中加入标识符号:CO_VARARGS,
    假设发现**key扩展键參数的形式,会向co_flags中加入 CO_VARKEYWORDS



    PyObject *
    PyEval_EvalCodeEx(PyCodeObject *co, PyObject *globals, PyObject *locals,
               PyObject **args, int argcount, PyObject **kws, int kwcount,
               PyObject **defs, int defcount, PyObject *closure)
    {
    	register PyFrameObject *f;
    	register PyObject **fastlocals, **freevars;
    	PyThreadState *tstate = PyThreadState_GET();
    	PyObject *x, *u;
    	//创建PyFrameObject对象
    	f = PyFrame_New(tstate, co, globals, locals);
    	fastlocals = f->localsplus;
    	freevars = f->f_localsplus + f->f_nlocals;
    	//[1]:推断是否须要处理扩展位置參数或扩展键參数
    	if(co->co_argcount > 0 || co->co_flags & (CO_VARARGS | CO_VARKEYWORDS)){
    		int i;
    		int n = argcount;
    		
    		if(argcount > co->co_argcount ){
    			n = co->co_argcount;
    		}
    		//[2]:设置位置參数的參数值
    		for(i = 0; i < n; i++){
    			x = args[i];
    			SETLOCAL(i, x);
    		}
    		//[3]:处理扩展位置參数
    		if(co->co_flags & CO_VARARGS){
    			//[4]:将PyTupleObject对象放入到f_localsplus中
    			u = PyTuple_New(argcount - n );
    			SETLOCAL(co->co_argcount, u);
    			//[5]:将扩展位置參数放入到PyTupleObject中
    			for( i = n; i < argcount; i++){
    				x = args[i];
    				PyTuple_SET_ITEM(u, i - n, x);
    			}
    		}
    	}
    	
    }


    Python虚拟机会在函数的PyCodeObject对象的变量名对象表(co_varnames)中查找键參数的名字,仅仅有在查找失败时,才干
    确定该键參数应该属于一个扩展键參数。
    PyObject *
    PyEval_EvalCodeEx(PyCodeObject *co, PyObject *globals, PyObject *locals,
               PyObject **args, int argcount, PyObject **kws, int kwcount,
               PyObject **defs, int defcount, PyObject *closure)
    {
    	//...
    	if(co->co_flags > 0 || co->co_flags & (CO_VARARGS | CO_VARKEYWORDS)){
    		int i;
    		int n = argcount;
    		PyObject *kwdict = NULL;
    		//...
    		//[1]:创建PyDictObject对象,并将其放到f_localsplus中
    		if(co->co_flags & CO_VARKEYWORDS){
    			kwdict = PyDict_New();
    			i = co->co_argcount;
    			//[2]:PyDictObject对象必须在PyTupleObject之后
    			if(co->co_flags & CO_VARARGS)
    				i++;
    			SETLOCAL(i, kwdict);
    		}
    		//遍历键參数,确定函数的def语句中是否出现了键參数的名字
    		for(i = 0; i < kwcount; i++){
    			PyObject *keyword = kws[2 * i];
    			PyObject *value = kws[2 * i + 1];
    			int j;
    			//在函数的变量名表中寻找keyword
    			for(j = 0; j < co->co_argcount; j++){
    				PyObject *nm = PyTuple_GET_ITEM(co->co_varnames, j);
    				int cmp = PyObject_RichCompareBool(keyword, nm, Py_EQ);
    				if(cmp > 0)//在co_varnames中找到keyword
    					break;
    				else if (cmp < 0)
    					goto fail;
    			}
    			
    			//[3]:keyword没有在变量名对象表中出现
    			if(j >= co->co_argcount){
    				PyDict_SetItem(kwdict, keyword, value);
    			}
    			//keyword在变量名对象表中出现
    			else{
    				SETLOCAL(j, value);
    			}
    			
    		}
    	}
    }



    6.函数中局部变量的訪问
    局部变量也是利用LOAD_FAST和 STORE_FAST来操作的,也是存放在f_localsplus中执行时栈前面的那段内存空间中。
    函数的实现中没有使用local名字空间,是由于函数中的局部变量总是固定不变的,编译时就能确定,不用动态查找。
    ??那local名字空间还有什么用?


    7.嵌套函数、闭包和decorator
    名字空间-->动态-->可将其静态化-->闭包
    闭包:名字空间与函数绑定后的结果,通常利用嵌套函数来完毕。


    co_cellvars: tuple,保存嵌套作用域中使用的变量名
    co_freevars: tuple,保存外部作用域中的变量名
    f_localsplus指向的内存有四部分:执行时栈、局部变量、cell对象、free对象


    外部函数的co_cellvars记录着嵌套作用域中使用的变量名,在创建内部函数的时候,将
    co_cellvars中的符号相应的值保存到内存函数的f_localsplus的cell对象中,从而实现闭包。
    在訪问的时候


    def get_func():
    LOAD_CONST 0
    MAKE_FUNCTION
    STORE_NAME 0
    	value = "inner"
    	LOAD_CONST 1
    	STORE_DEREF 0
    	def inner_func():
    	LOAD_CLOSUER 0
    	BUILD_TUPLE
    	LOAD_CONST 2
    	MAKE_CLOSURE 0
    	STORE_FAST 0
    		print value
    		LOAD_DEREF 0
    		PRINT_ITEM
    		PRINT_NEWLINE
    		LOAD_CONST 0
    		RETURN_VALUE
    	return inner_func
    	LOAD_FAST 0
    	RETURN_VALUE
    show_value = get_func()
    LOAD_NAME 0
    CALL_FUNCTION 0
    STORE_NAME 1
    show_value()
    LOAD_NAME 1
    CALL_FUNCTION 0
    POP_TOP
    LOAD_CONST
    RETURN_VALUE

    在PyEval_EvalCodeEx中,Python虚拟机会如同处理默认參数一样,将co_cellvars中的东西复制到新建的PyFrameObject的
    f_localsplus中


    定义时


    调用时



    Decorator --> 好像是回调函数
    在python中,decorator是func = should_say(func)的一种包装方式,理解decorator的关键在于理解closure
    def should_say(fn):
    	def say(*args):
    		print 'say something ...'
    		fn(*args)
    	return say
    @should_say
    def func():
    	print 'in func'

    和以下的事实上是一样的。
    #...
    def func():
    	print 'in func'
    func = should_say(func)
    func()
    


  • 相关阅读:
    【BZOJ3533】向量集(SDOI2014)-线段树+凸壳+二分
    【BZOJ4869】相逢是问候(六省联考2017)-扩展欧拉定理+线段树
    【BZOJ4012】开店(HNOI2015)-动态点分治+set
    【BZOJ1095】捉迷藏(ZJOI2007)-动态点分治+堆
    【BZOJ2299】向量(HAOI2011)-裴蜀定理
    【BZOJ4942】整数(NOI2017)-线段树+压位
    【BZOJ3594】方伯伯的玉米田(SCOI2014)-DP+二维树状数组
    背包DP专题
    【2018.11.7】【luoguNOIp 热身赛】解题报告及总结
    【一天一DP计划】状压DP
  • 原文地址:https://www.cnblogs.com/mfrbuaa/p/4341529.html
Copyright © 2011-2022 走看看