《Python源码剖析》读书笔记-11 Python虚拟机中的函数机制


第11章 Python虚拟机中的函数机制

  • 函数也是一个对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    typedef struct {
    PyObject_HEAD
    PyObject *func_code; /* A code object */
    PyObject *func_globals; /* A dictionary (other mappings won't do) */
    PyObject *func_defaults; /* NULL or a tuple */
    PyObject *func_closure; /* NULL or a tuple of cell objects */
    PyObject *func_doc; /* The __doc__ attribute, can be anything */
    PyObject *func_name; /* The __name__ attribute, a string object */
    PyObject *func_dict; /* The __dict__ attribute, a dict or NULL */
    PyObject *func_weakreflist; /* List of weak references */
    PyObject *func_module; /* The __module__ attribute, can be anything */

    /* Invariant:
    * func_closure contains the bindings for func_code->co_freevars, so
    * PyTuple_Size(func_closure) == PyCode_GetNumFree(func_code)
    * (func_closure may be NULL if PyCode_GetNumFree(func_code) == 0).
    */

    } PyFunctionObject;
  • PyCodeObject和PyFunctionObject的区别:

    • PyCodeObject是编译时产生的静态对象,里面的所有信息都可以从源代码中获得;
    • PyFunctionObject则是程序在运行时动态产生的(是下面即将提到的MAKE_FUNCTION指令产生的),其中会包括一个静态产生的PyCodeObject,也就是func_code域,这个对象对应了函数的代码块。除此之外,func_globals之类的上下文信息是在运行时才能确定的动态信息。
  • 在定义函数时,会生成嵌套的PyCodeObject结构。函数的代码块对应的PyCodeObject会存储于外层PyCodeObject的常量表中。这样,外层PyCodeObject的字节码指令中只包含了’def f:’所对应的字节码指令(MAKE_FUNCTION),而f内代码块赌赢的字节码指令则不直接包含在外层PyCodeObject中,这样,在执行函数的定义时,函数就不会被调用;只有当真正调用函数时,函数内的代码(对应的字节码指令)才会真正被执行。

  • MAKE_FUNCTION创建函数对象,需要先从外层PyCodeObject对象的常量表(co_consts)中取出当前所定义函数对应的PyCodeObject对象,然后以此PyCodeObject对象和当前栈帧的globals名字空间为参数,具体的顶一个PyFunctionObject。之后将函数名(例如,’f’)和这个PyFunctionObject对象对应起来,存入当前栈帧的locals名字空间。

  • 函数调用:

    • 首先依据函数名,从当前栈帧的locals名字空间中取出PyFunctionObject对象,压入运行时栈;
    • 然后执行字节码指令CALL_FUNCTION。
    • 首先处理参数信息;
    • 然后从运行时栈中获得刚才被压入的函数对象。
    • 接着从函数对象中拿到PyCodeObject对象,globals名字空间等信息,用这些信息新建一个栈帧对象PyFrameObject,调用PyEval_EvalFrameEx(递归的)执行栈帧上的字节码指令。并将执行结果返回。
    • 至此,函数的调用完成。
  • 参数传递

    • CALL_FUNCTION指令的参数oparg,低字节记录了位置参数的个数,高字节记录了键参数的个数。所以可以看到如下代码取出相关信息。其中也可以看出,一个位置参数占据运行时栈的一个slot,而一个键参数则会占据两个slot(一个键,一个值)。

      1
      2
      3
      4
      int na = oparg & 0xff;
      int nk = (oparg>>8) & 0xff;
      int n = na + 2 * nk;
      PyObject **pfunc = (*pp_stack) - n - 1;
    • 对于位置参数,会在调用函数之前,首先将参数压入运行时栈中,然后在生成新的栈帧PyFrameObject之后(递归调用PyEval_EvalFrameEx之前),把参数从运行时栈中拷贝到PyFrameObject.f_localsplus中。

    • 位置参数的读取也很简单。因为它是从左到右依次存储在f_localsplus中的,所以读取时只需给出其位置索引即可。
    • 位置参数的默认值,会在MAKE_FUNCTION的过程中保存在PyFunctionObject.func_defaults这个对象中,以键值对的形式(在所有有默认值的位置参数中的索引–>默认值)。
    • 之后,在调用有默认值的函数时,会进入与之前不同的执行路径PyEval_EvalCodeEx,其中会依据传入的位置参数个数、总共的位置参数个数、具有默认值的参数个数来对参数的数值进行设定,并将参数的具体值放入f_localsplus的对应位置中去。
    • 对于键参数来说,会首先在函数调用前,将键参数的键和值依次压入运行时栈,在构造新的栈帧对象PyFrameObject时,取出参数名对应的字符串,在函数对应的PyCodeObject对象的co_varnames域中查找(co_varnames域在python编译时就保存了函数定义时的所有参数的名字),查找到之后,按照其在co_varnames中的索引,设置PyFrameObject.f_localsplus值。而在其后设置默认值时,对于已经被键参数设置的函数参数,会检测到其已经存在于PyFrameObject.f_localsplus中,就不再为其设置默认值。
    • 扩展位置参数与扩展键参数都是被当做局部变量来处理的。其中对于扩展位置参数,会生成一个PyTupleObject放入f_localsplus的对应位置;对于扩展键参数,会生成一个PyDictObject,收集了那些调用时传递给函数的键值对中键没有出现在co_varnames中的那些键值对,并将此PyDictObject置于f_localsplus的特定位置中。
  • 函数的局部变量,并没有存储于当前栈帧的f_locals中。实际上,f_locals根本就是一个NULL值。而函数的局部变量也是存放在f_localsplus这样一个数组中的。这是因为在函数的编译过程中,函数中的局部变量信息是完全清楚的。因此,使用f_localsplus这样的数组结构就可以完成功能,而且比那种从f_locals字典中查找的方式更高效。

  • 闭包(Closure)

    • 在PyCodeObject中,co_cellvars保存了内层嵌套作用域(如果有的话)中使用的本层变量名的集合;co_freevars保存了本层作用域中使用了外层作用域中的变量名的集合。
    • 在PyFrameObject中,f_localsplus中也保存了cellvars和freevars。具体的,在生成PyFrameObject之后,会把对应的PyCodeObject中的cellvars生成PyCellObject对象,复制到f_localsplus中的局部变量的后面。
    • 在定义闭包之时(也就是外层函数执行到内层函数的定义处的时候),会把cellvars对应的PyCellObject打包成一个tuple、内层函数对应的PyCodeObject对象压入运行时栈,然后执行一个MAKE_CLOSURE指令。在MAKE_CLOSURE指令的执行中,除了新生成一个PyFunctionObject对象之外,还将运行时栈中的那个tuple赋给了PyFunctionObject对象的func_closure域;而在执行内层函数时,就会将PyFunctionObject中的func_closure中的对象依次取出,放入f_localsplus中的对应位置(在局部变量和cellvars之后)。当需要用到这些来自外层函数的变量时,就会从f_localsplus中的freevars区域取出。
  • 装饰器(Decorator)

    • 装饰器的实现是基于闭包的,两者编译过后的字节码执行效果完全相同,因此装饰器不过是闭包的一种语法糖而已。

欢迎关注我的微信公众号,技术·生活·思考:
后端技术小黑屋

Comments