《python解释器源码剖析》第12章--python虚拟机中的函数机制

Posted 来自东方地灵殿的小提琴手

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《python解释器源码剖析》第12章--python虚拟机中的函数机制相关的知识,希望对你有一定的参考价值。

12.0 序

函数是任何一门编程语言都具备的基本元素,它可以将多个动作组合起来,一个函数代表了一系列的动作。当然在调用函数时,会干什么来着。对,要在运行时栈中创建栈帧,用于函数的执行。

在python中,PyFrameObject对象就是一个对栈帧的模拟,所以我们即将也会看到,python虚拟机在执行函数调用时会动态地创建新的PyFrameObject对象。随着函数调用链的增长,这些PyFrameObject对象之间也会形成一条PyFrameObject对象链,这条链就是对象x86平台上运行时栈的模拟

12.1 PyFunctionObject对象

我们说过python中一切皆对象,函数也不例外。在python中,函数这种抽象机制是通过PyFunctionObject对象实现的

typedef struct {
    PyObject_HEAD  //头部信息,不用多说
    PyObject *func_code;        /* 对应的函数编译后的PyCodeObject对象 */
    PyObject *func_globals;     /* 函数运行时的global命名空间 */
    PyObject *func_defaults;    /* 默认参数,tuple或者NULL */
    PyObject *func_kwdefaults;  /* 关键字默认参数,dict或者NULL */
    PyObject *func_closure;     /* 用于实现闭包 */
    PyObject *func_doc;         /* 函数的文档,PyUnicodeObject对象 */
    PyObject *func_name;        /* 函数名,PyUnicodeObject对象 */
    PyObject *func_dict;        /* 属性字典,dict或者NULL */
    PyObject *func_weakreflist; /* 弱引用列表 */
    PyObject *func_module;      /* 函数的模块 */
    PyObject *func_annotations; /* 注解 */
    PyObject *func_qualname;    /* 和name类似 */
} PyFunctionObject;

我们通过python来看看这些属性吧,首先这些属性都是func_xxx格式的,在python中可以通过__xxx__的形式获取

  • func_code:对应的字节码

    def foo():
        pass
    
    
    print(foo.__code__)  # <code object foo at 0x000001DE153DD3A0, file "C:/Users/satori/Desktop/love_minami/a.py", line 1>
    
  • func_globals:global命名空间

    name = "xxx"
    
    
    def foo():
        pass
    
    
    # __globals__其实就是外部的globals
    print(foo.__globals__["name"])  # xxx
    print(foo.__globals__ == globals())  # True
    
  • func_defaults:默认参数

    def foo(name="satori", age=16):
        pass
    
    
    print(foo.__defaults__)  # (\'satori\', 16)
    
    
    def bar():
        pass
    
    
    print(bar.__defaults__)  # None
    
  • func_kwdefaults:默认的关键字参数。

    怎么理解呢?意思就是这些参数不仅仅有默认值,而且还必须要通过关键字的方式传递

    def foo(name="satori", age=16):
        pass
    
    
    # 打印是为None的,这是因为虽然有默认值,但是它并不要求必须通过关键字的方式传递
    print(foo.__kwdefaults__)  # None
    
    
    # 我在前面加上一个*,表示后面的参数就必须通过关键字的方式传递
    # 因为如果不通过关键字的话,那么无论多少个位置参数都会被*给吸收掉
    # 无论如何也是传递不到name,age的
    # 我们经常会看到*args,这是因为我们需要这些参数,所以可以通过args来拿到这些参数
    # 但是这里我们不需要,我们只是希望后面的参数必须通过关键字参数传递,因为前面写一个*即可
    # 当然写*args或者其他的也可以,但是我们用不到,所以写一个*即可
    def bar(*, name="satori", age=16):
        pass
    
    
    # 此时就打印了默认值,因为这是只能通过kw(关键字)传递的参数的默认值
    print(bar.__kwdefaults__)  # {\'name\': \'satori\', \'age\': 16}
    
  • func_closure:闭包

    def foo():
        x = 123
        y = 456
    
        def bar():
            nonlocal x
            nonlocal y
    
        return bar
    
    
    # 查看的是闭包里面nonlocal的值
    # 这里有两个nonlocal,所以foo().__closure__是一个有两个元素的元组
    print(foo().__closure__)  # (<cell at 0x000001DF68BE5BB0: int object at 0x00007FFE96C9A5E0>, <cell at 0x000001DF68BFFF10: int object at 0x000001DF68BF6C10>)
    print(foo().__closure__[0].cell_contents)  # 123
    print(foo().__closure__[1].cell_contents)  # 456
    
    # 注意:查看闭包属性我们使用的是内层函数,不是外层的foo
    
  • func_doc:函数的文档

    def foo(name, age):
        """
        接收一个name和age,
        返回一句话
        my name is $name, age is $age
        """
        return f"my name is {name}, age is {age}"
    
    
    print(foo.__doc__)
    """
    
        接收一个name和age,
        返回一句话
        my name is $name, age is $age
        
    """
    
  • func_name:函数名

    def foo(name, age):
        pass
    
    
    print(foo.__name__)  # foo
    
  • func_dict:属性字典

    def foo(name, age):
        pass
    
    
    # 一般函数的属性字典都会空,属性字典基本上在类里面使用
    print(foo.__dict__)  # {}
    
  • func_weakreflist:弱引用列表

    python无法获取这个属性,没有提供相应的接口

  • func_module:函数所在的模块

    def foo(name, age):
        pass
    
    
    print(foo.__module__)  # __main__
    
  • func_annotations:注解

    def foo(name: str, age: int):
        pass
    
    
    print(foo.__annotations__)  # {\'name\': <class \'str\'>, \'age\': <class \'int\'>}
    
  • func_qualname:和func_name类似

    def foo(name: str, age: int):
        pass
    
    
    print(foo.__qualname__)  # foo
    

PyFunctionObject对象中,我们看到了很多域。但是有两个域都和函数有关:PyCodeObject对象和PyFunctionObject对象,这两个对象非常重要。PyCodeObject对象是对一段代码的静态表示,python对源代码编译之后,会对每一个code block都生成一个、且唯一一个PyCodeObject,这个PyCodeObject对象中包含了这个code block中的一些静态信息,所谓静态信息是可以从源代码中看到的信息。比如code block中有一个a = 1这样的表达式,那么符号a和值1、以及它们之间的联系就是一种静态的信息,这些信息会分别存在PyCodeObject对象的常量池:co_consts、符号表:co_names、以及字节码序列:co_code中,这些信息是编译的时候就可以得到的,因此PyCodeObject对象是编译时候的结果。

而PyFunctionObject则不同,PyFunctionObject对象是python代码在运行时动态产生的,更准确的说,是在执行一个def语句的时候创建的。在PyFunctionObject对象中,也会包含这些函数的静态信息,这些信息存储在func_code中,实际上,func_code一定会指向与函数代码对应的PyCodeObject对象。除此之外,PyFunctionObject对象中还包含了一些函数在执行是所必须的动态信息,即上下文信息,比如func_globals,就是函数在执行时关联的global作用域(globals)。global作用域中的符号和值必须在运行时才能确定,所以这部分必须在运行时动态创建,无法存储在PyCodeObject中。

对于一段python代码,其对应的PyCodeObject对象只有一个,但是代码对应的PyFunctionObject对象却可以有多个,比如一个函数多次调用,python会在运行时创建多个PyFunctionObject对象,而每一个PyFunctionObject对象的func_code域都会关联到这个PyCodeObject

12.2 无参函数调用

12.2.1 函数对象的创建

我们先从无参的函数调用开始,因为这是最简单的。

a.py

def foo():
    print("this is a function")


foo()

b.py

import dis
code = compile(open("a.py", encoding="utf-8").read(), "a.py", "exec")
dis.dis(code)
"""
  1           0 LOAD_CONST               0 (<code object foo at 0x000002807F87EF50, file "a.py", line 1>)
              2 LOAD_CONST               1 (\'foo\')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (foo)

  5           8 LOAD_NAME                0 (foo)
             10 CALL_FUNCTION            0
             12 POP_TOP
             14 LOAD_CONST               2 (None)
             16 RETURN_VALUE

Disassembly of <code object foo at 0x000002807F87EF50, file "a.py", line 1>:
  2           0 LOAD_GLOBAL              0 (print)
              2 LOAD_CONST               1 (\'this is a function\')
              4 CALL_FUNCTION            1
              6 POP_TOP
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE

"""

显然这个代码中出现了两个PyCodeObject对象,一个对应整个py文件,另一个则是对应函数foo。我们看到上面源代码的1和5行是py文件对应的PyCodeObject对象,而第2行则是对应函数foo的PyCodeObject对象

code = compile(open("a.py", encoding="utf-8").read(), "a.py", "exec")
print(type(code))  # <class \'code\'>
print(type(code.co_consts[0]))  # <class \'code\'>
print(code.co_name)  # <module>
print(code.co_consts[0].co_name)  # foo

可以看到,函数foo对应的PyCodeObject对象是a.py这个模块对应的PyCodeObject对象的常量池co_consts中的一个元素。因为在对a.py创建PyCodeObject对象的时候,发现了一个函数foo,那么会对函数foo继续创建一个PyCodeObject对象(每一个代码块都会对应一个PyCodeObject对象),而函数foo对应的PyCodeObject对象则是模块a.py对应的PyCodeObject对象的co_consts常量池当中的一个元素。我们可以看一个再复杂一点的例子

# a1.py
def foo():
    def bar():
        print("this is a function")


def foo1():
    pass

foo()
# 首先code是什么?显然是a1.py对应的PyCodeObject对象
code = compile(open("a1.py", encoding="utf-8").read(), "a.py", "exec")

# 而foo和foo1显然是模块级别的函数,那么这两位应该都是模块对应的PyCodeObject对象的常量池里面的元素
print(code.co_consts[0].co_name)  # foo
print(code.co_consts[2].co_name)  # foo1
# 至于索引、也就是在常量池中的位置,我们目前不需要关心,只要确定在常量池里面即可

# 而我们看到foo里面还嵌入了一个bar,这是一个闭包。
# 然而即便如此,它毕竟是在foo里面,那么按照之前的逻辑,显然bar对应的PyCodeObject对象也应该在foo对应的PyCodeObject对象的co_consts中
# code.con_consts[0]是foo对应的PyCodeObject对象,那么code.con_consts[0].co_consts[0]是不是就是bar对应的字节码对象呢
# 答案是猜对了一半,确实在里面,只不过索引不是0,而是1
print(code.co_consts[0].co_consts[1].co_name)  # bar

# 还是那句话,我们只是确定位置,至于顺序,也就是这里的索引,我们暂时不追究

通过以上例子,我们发现,字节码是嵌套的。在介绍字节码对象的时候,我们说了,每一个code block(函数、类等等)都会创建一个字节码对象。现在我们又看到了,根据层级来分的话,内层代码块对应的PyCodeObject对象是最近的外层代码块对应的PyCodeObject对象的常量池co_consts中的一个元素。而最外层则是模块对应的PyCodeObject对象,因此这就意味着我们通过最外层的PyCodeObject对象可以找到所有的PyCodeObject,显然这是毋庸置疑的。而这里和栈帧也是对应的,栈帧我们说过也是层层嵌套的。执行字节码的时候会创建对应的栈帧,而内层栈帧通过f_back可以找到外层、也就是调用者对应的栈帧,当然这里我们之前的章节已经说过了,这里再提一遍。

我们再来观察一下之前的a.py对应的字节码

  1           0 LOAD_CONST               0 (<code object foo at 0x000002807F87EF50, file "a.py", line 1>)
              2 LOAD_CONST               1 (\'foo\')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (foo)

  5           8 LOAD_NAME                0 (foo)
             10 CALL_FUNCTION            0
             12 POP_TOP
             14 LOAD_CONST               2 (None)
             16 RETURN_VALUE

Disassembly of <code object foo at 0x000002807F87EF50, file "a.py", line 1>:
  2           0 LOAD_GLOBAL              0 (print)
              2 LOAD_CONST               1 (\'this is a function\')
              4 CALL_FUNCTION            1
              6 POP_TOP
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE

我们发现之前看到源代码的行号都是从上往下、依次增大的,这很好理解,毕竟一条一条解释嘛。但是我们看到,这里对于函数来说,发生了变化,先执行了第五行,之后再执行第二行。如果是从python层面的函数调用来理解的话,很容易一句话就解释了,因为函数只有在调用的时候才会执行。但是从字节码的角度来理解的话,我们发现函数的声明和实现是分离的,是在不同的PyCodeObject对象中。确实如此,第一个行代码和第二行代码虽然是一个整体,但是python虚拟机在实现这个函数的时候,却在物理上将它们分离开了。第一行字节码指令序列必须在a.py对应的PyCodeObject对象中,这一点也很好理解。

因为我们之前说过,函数即变量。我们是可以把函数当成是普通的变量来处理的,函数名就相当于变量名,函数体就相当于是函数值。而foo函数显然是a.py的最外层中定义的一个函数,这就意味着我们可以通过a.foo找到它,那么foo是不是要出现在a.py对应的字节码(PyCodeObject)对象中符号表co_names里面呢?foo对应的PyCodeObject对象是不是要出现在a.py对应的字节码对象的常量池co_consts里面的。

code = compile(open("a1.py", encoding="utf-8").read(), "a.py", "exec")

print(code.co_names)  # (\'foo\', \'foo1\')

所以我们大致能理清逻辑了,每一个代码块都会创建一个PyCodeObject对象,那么0 LOAD_CONST显然就是将函数foo对应字节码load进来,在2 LOAD_CONST 将符号、或者说是变量名foo给load进来。然后调用4 MAKE_FUNCTION,这里的MAKE_FUNCTION我们暂时先不管,不过从名字也能看出来,这个之前的BUILD LIST比较类似,相当于使用字节码对象MAKE一个FUNCTION,然后6 STORE_NAME将"变量名->foo"和"变量值->FUNCTION"作为一个entry存储在命名空间中,这是显然是global命名空间。

然而这个函数是什么是构建的呢?显然是从def foo():这条代码处完成的。从语法上将这是函数的声明语句,但是从虚拟机的角度来看这其实是函数对象的创建语句。

两者是分离的,a.py对应的字节码对象中的foo指向foo对应的字节码,两者是分离的,当然这里的分离指的是两个不同的字节码对象。

python虚拟机在执行def语句时,会动态地创建一个函数,即PyFunctionObject对象,显然这是靠我们之前说的MAKE FUNCTION指令完成的。

//ceval.c        
		TARGET(MAKE_FUNCTION) {
            PyObject *qualname = POP(); //弹出符号表中的函数名
            PyObject *codeobj = POP(); //弹出对应的字节码对象
            //创建PyFunctionObject对象
            PyFunctionObject *func = (PyFunctionObject *)
                PyFunction_NewWithQualName(codeobj, f->f_globals, qualname);

            Py_DECREF(codeobj);
            Py_DECREF(qualname);
            if (func == NULL) {
                goto error;
            }
			
            //下面是设置闭包、注解等属性
            if (oparg & 0x08) {
                assert(PyTuple_CheckExact(TOP()));
                func ->func_closure = POP();
            }
            if (oparg & 0x04) {
                assert(PyDict_CheckExact(TOP()));
                func->func_annotations = POP();
            }
            if (oparg & 0x02) {
                assert(PyDict_CheckExact(TOP()));
                func->func_kwdefaults = POP();
            }
            if (oparg & 0x01) {
                assert(PyTuple_CheckExact(TOP()));
                func->func_defaults = POP();
            }
			
            //压入栈中
            PUSH((PyObject *)func);
            DISPATCH();
        }

我们看到在MAKE FUNCTION之前,先进行了LOAD CONST,显然是将foo对应的字节码对象和符号foo压入到了栈中。所以在执行MAKE FUNCTION的时候,首先就是将这个字节码对象以及对应符号弹出栈,然后再加上当前PyFrameObject对象中维护的global命名空间f_globals对象为参数,三者作为参数传入PyFunction_NewWithQualName函数中,从而构建出相应的PyFunctionObject对象。而这个f_globals就是函数foo在运行时的global命名空间,而函数的global命名空间和模块级别的global是一样的,当然和模块级别的local命名空间也是一样的,因为对于模块来说,local和global是一样的。

def foo():
    def bar():
        return globals()

    return bar


# 即使当中嵌入了一个闭包,我们的结论依旧是正确的
print(foo()() == globals() == locals())  # True

下面我们来看看PyFunction_NewWithQualName是如何构造出一个函数的。

//Object/funcobject.c
PyObject *
PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname)
{	
    //要返回的PyFunctionObject,这里先声明一下
    PyFunctionObject *op;
    //doc、consts、module这些我们之前都见过的属性
    PyObject *doc, *consts, *module;
    //函数名
    static PyObject *__name__ = NULL;

    if (__name__ == NULL) {
        __name__ = PyUnicode_InternFromString("__name__");
        if (__name__ == NULL)
            return NULL;
    }
	
    //为PyFunctionObject申请内存空间,类型为function
    op = PyObject_GC_New(PyFunctionObject, &PyFunction_Type);
    if (op == NULL)
        return NULL;
	
    //下面都是设置相关属性
    op->func_weakreflist = NULL;
    Py_INCREF(code);
    op->func_code = code;
    Py_INCREF(globals);
    op->func_globals = globals;
    op->func_name = ((PyCodeObject *)code)->co_name;
    Py_INCREF(op->func_name);
    op->func_defaults = NULL; /* No default arguments */
    op->func_kwdefaults = NULL; /* No keyword only defaults */
    op->func_closure = NULL;

    consts = ((PyCodeObject *)code)->co_consts;
    if (PyTuple_Size(consts) >= 1) {
        doc = PyTuple_GetItem(consts, 0);
        if (!PyUnicode_Check(doc))
            doc = Py_None;
    }
    else
        doc = Py_None;
    Py_INCREF(doc);
    op->func_doc = doc;

    op->func_dict = NULL;
    op->func_module = NULL;
    op->func_annotations = NULL;

    module = PyDict_GetItem(globals, __name__);
    if (module) {
        Py_INCREF(module);
        op->func_module = module;
    }
    if (qualname)
        op->func_qualname = qualname;
    else
        op->func_qualname = op->func_name;
    Py_INCREF(op->func_qualname);

    _PyObject_GC_TRACK(op);
    return (PyObject *)op;
}

在创建了PyFunctionObject对象之后,MAKE FUNCTION还会进行一些处理函数参数的动作,由于目前的foo是一个无参函数,所以这里暂时先略过。在MAKE FUNCTION之后,新建的PyFunctionObject对象就被会压入栈中,然后下面的6 STORE_NAME则显然是将fooPyFunctionObject对象组合成一个entry存储在global命名空间中。

12.2.2 函数调用

下面我们来看函数是如何调用的。首先肯定要8 LOAD_NAME,把foo对应的value加载进来,然后就是我们熟悉的CALL_FUNCTION,之前在print的时候就已经遇见了,但只是随便提一下。

  1           0 LOAD_CONST               0 (<code object foo at 0x000002807F87EF50, file "a.py", line 1>)
              2 LOAD_CONST               1 (\'foo\')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (foo)

  5           8 LOAD_NAME                0 (foo)
             10 CALL_FUNCTION            0
             12 POP_TOP
             14 LOAD_CONST               2 (None)
             16 RETURN_VALUE

Disassembly of <code object foo at 0x000002807F87EF50, file "a.py", line 1>:
  2           0 LOAD_GLOBAL              0 (print)
              2 LOAD_CONST               1 (\'this is a function\')
              4 CALL_FUNCTION            1
              6 POP_TOP
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE

那么现在我们是时候该研究一下这个CALL_FUNCTION了,究竟生得哪般模样!

        PREDICTED(CALL_FUNCTION);
        TARGET(CALL_FUNCTION) {
            PyObject **sp, *res;
            //获取运行时栈栈顶指针
            sp = stack_pointer;
            //直接杀入call_function,我们看到sp是一个二级指针
            //又传入了&sp,那么call_function的第一个参数应该是一个三级指针
            res = call_function(&sp, oparg, NULL);
            stack_pointer = sp;
            PUSH(res);
            if (res == NULL) {
                goto error;
            }
            DISPATCH();
        }

#define PyCFunction_Check(op) (Py_TYPE(op) == &PyCFunction_Type)
#define PyFunction_Check(op) (Py_TYPE(op) == &PyFunction_Type)

//ceval.c
Py_LOCAL_INLINE(PyObject *) _Py_HOT_FUNCTION
call_function(PyObject ***pp_stack, Py_ssize_t oparg, PyObject *kwnames)
{	
    //获取PyFunctionObject对象,因为pp_stack是在CALL_FUNCTION指令中传入的栈顶指针
    //传入的oparg是0,kwnames是NULL,这里的pfunc就是MAKE_FUNCTION中创建的PyFunctionObject对象
    PyObject **pfunc = (*pp_stack) - oparg - 1;
    //因此这里的func和pfunc是一样的
    PyObject *func = *pfunc;
    PyObject *x, *w;
    //处理参数,对于我们当前的函数来说,这里的nkwargs和nargs都是0
    Py_ssize_t nkwargs = (kwnames == NULL) ? 0 : PyTuple_GET_SIZE(kwnames);
    Py_ssize_t nargs = oparg - nkwargs;
    PyObject **stack = (*pp_stack) - nargs - nkwargs;

    /* Always dispatch PyCFunction first, because these are
       presumed to be the most frequent callable object.
    */
    //我们看到这里还有cfunction,这个cfunction是什么先不管
    if (PyCFunction_Check(func)) {
        PyThreadState *tstate = PyThreadState_GET();
        C_TRACE(x, _PyCFunction_FastCallKeywords(func, stack, nargs, kwnames));
    }
    //这里的method这不需要关心
    else if (Py_TYPE(func) == &PyMethodDescr_Type) {
        PyThreadState *tstate = PyThreadState_GET();
        if (nargs > 0 && tstate->use_tracing) {
            PyObject *self = stack[0];
            func = Py_TYPE(func)->tp_descr_get(func, self, (PyObject*)Py_TYPE(self));
            if (func != NULL) {
                C_TRACE(x, _PyCFunction_FastCallKeywords(func,
                                                         stack+1, nargs-1,
                                                         kwnames));
                Py_DECREF(func);
            }
            else {
                x = NULL;
            }
        }
        else {
            x = _PyMethodDescr_FastCallKeywords(func, stack, nargs, kwnames);
        }
    }
    else {
        if (PyMethod_Check(func) && PyMethod_GET_SELF(func) != NULL) {
            PyObject *self = PyMethod_GET_SELF(func);
            Py_INCREF(self);
            func = PyMethod_GET_FUNCTION(func);
            Py_INCREF(func);
            Py_SETREF(*pfunc, self);
            nargs++;
            stack--;
        }
        else {
            Py_INCREF(func);
        }
		
        //这里是关键,通过_PyFunction_FastCallKeywords对PyFunctionObject对象进行调用
        //传入func, stack, nargs, kwnames,并把返回结果赋值给了PyObject *x
        if (PyFunction_Check(func)) {
            x = _PyFunction_FastCallKeywords(func, stack, nargs, kwnames);
        }
        else {
            x = _PyObject_FastCallKeywords(func, stack, nargs, kwnames);
        }
        Py_DECREF(func);
    }

    assert((x != NULL) ^ (PyErr_Occurred() != NULL));

    while ((*pp_stack) > pfunc) {
        w = EXT_POP(*pp_stack);
        Py_DECREF(w);
    }

    return x;
}

python虚拟机通过PyFunction_Check进行检查之后,就会进入_PyFunction_FastCallKeywords。这里需要关注的是里面的第一个参数func,同时这个func也是需要被上面PyFunction_Check检查的对象,显然这个func,就是通过def foo:这个代码块对应的PyCodeObject创建的PyFunctionObject对象。

PyObject *
_PyFunction_FastCallKeywords(PyObject *func, PyObject *const *stack,
                             Py_ssize_t nargs, PyObject *kwnames)
{	
    //获取PyFunctionObject的字节码
    PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func);
    //获取global命名空间
    PyObject *globals = PyFunction_GET_GLOBALS(func);
    //默认参数
    PyObject *argdefs = PyFunction_GET_DEFAULTS(func);
    //一些其他属性
    PyObject *kwdefs, *closure, *name, *qualname;
    PyObject **d;
    Py_ssize_t nkwargs = (kwnames == NULL) ? 0 : PyTuple_GET_SIZE(kwnames);
    Py_ssize_t nd;
	
    //检测
    assert(PyFunction_Check(func));
    assert(nargs >= 0);
    assert(kwnames == NULL || PyTuple_CheckExact(kwnames));
    assert((nargs == 0 && nkwargs == 0) || stack != NULL);
	
    //我们观察一下下面的return
    //一个是function_code_fastcall,一个是最后的_PyEval_EvalCodeWithName
    //由于我们的函数没有参数,因此这里走的是快速通道
    //function_code_fastcall里面逻辑很简单,直接抽走当前PyFunctionObject里面PyCodeObject和函数运行时的global命名空间等信息
    //根据PyCodeObject对象直接为其创建一个PyFrameObject对象,然后PyEval_EvalFrameEx执行栈帧
    //也就是真正的进入了函数调用,执行函数里面的代码
    if (co->co_kwonlyargcount == 0 && nkwargs == 0 &&
        (co->co_flags & ~PyCF_MASK) == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE))
    {
        if (argdefs == NULL && co->co_argcount == nargs) {
            return function_code_fastcall(co, stack, nargs, globals);
        }
        else if (nargs == 0 && argdefs != NULL
                 && co->co_argcount == PyTuple_GET_SIZE(argdefs)) {
            stack = &PyTuple_GET_ITEM(argdefs, 0);
            return function_code_fastcall(co, stack, PyTuple_GET_SIZE(argdefs),
                                          globals);
        }
    }

    kwdefs = PyFunction_GET_KW_DEFAULTS(func);
    closure = PyFunction_GET_CLOSURE(func);
    name = ((PyFunctionObject *)func) -> func_name;
    qualname = ((PyFunctionObject *)func) -> func_qualname;

    if (argdefs != NULL) {
        d = &PyTuple_GET_ITEM(argdefs, 0);
        nd = PyTuple_GET_SIZE(argdefs);
    }
    else {
        d = NULL;
        nd = 0;
    }
    //如果有参数的话,现在会走这一步,逻辑会复杂一些,不过这些都是后话了。
    //但是显然最终也会经过PyEval_EvalFrameEx
    return _PyEval_EvalCodeWithName((PyObject*)co, globals, (PyObject *)NULL,
                                    stack, nargs,
                                    nkwargs ? &PyTuple_GET_ITEM(kwnames, 0) : NULL,
                                    stack + nargs,
                                    nkwargs, 1,
                                    d, (int)nd, kwdefs,
                                    closure, name, qualname);
}

因此我们看到,总共有两条路径,分别针对无参和有参,但是最终殊途同归、都会走到PyEval_EvalFrameEx那里。然后虚拟机在新的栈帧中执行新的PyCodeObject,而这个PyCodeObject就是函数对应的PyCodeObject,也就是函数foo里面的那条print语句对应的字节码

但是到这里恐怕就有人有疑问了,我们之前说过PyFrameObject是根据PyCodeObject创建的,而PyFunctionObject也是根据PyCodeObject创建的,那么PyFrameObject和PyFunctionObject之间有啥关系呢?如果把PyCodeObject比喻成"男人"的话,那么PyFunctionObject就是男人的"基友",PyFrameObject就是男人的"媳妇"。其实在PyEval_EvalFrameEx执行栈帧的时候,PyFunctionObject的影响就已经消失了,真正对栈帧产生影响的是PyFunctionObject里面的PyCodeObject对象和global命名空间。也就是说,PyFunctionObject辛苦一场,实际上是为别人做了嫁衣。PyFunctionObject主要是对字节码和global命名空间的一种打包和运输方式。

我们之前提到了快速通道,那么函数是通过什么来判断是否可以进入快速通道呢?答案是通过函数参数的形式来决定是否可以进入快速通道

12.3 函数执行时的命名空间

现在我们对函数调用机制有了一个大致的认识,另外我们发现在最终的函数调用时,有一个参数叫globals,这个globals最终成为和函数foo对应的PyFrameObject对应的global命名空间:f_globals

另外还记得当初对LOAD_NAME指令的分析吗?我们说过在执行该指令时,python会依次从:f_localsf_globalsf_builtins中进行搜索。在function_code_fastcall中传入的globals将成为在新的栈帧中执行函数foo时候的global命名空间。而在执行MAKE_FUNCTION的指令代码中(对应函数),这个globals就是当前PyFrameObject对象(对应模块)中的f_globals。这就意味着,执行a.py字节码指令序列时对应的global命名空间和执行函数foo字节码指令序列时对应的global命名空间实际上是一个命名空间。实际上这个命名空间是通过PyFunctionObject对象的携带,和字节码指令序列对应的PyCodeObject对象一起被传入到新的栈帧当中的。

但是为什么要将globals传到新创建的栈帧当中呢?这不废话吗?只有传了globals,函数内部才能在找不到变量的时候去外部找啊,如果你globals都不传,那不就为空了吗?即便外面有,你也找不到啊。

另外,我们说创建变量,会把符号和值作为一个entry放到f_locals里面的,但是对于模块级别的函数来说,f_locals和f_globals指向的是一个东西,因为模块已经是最外层了,也就不会有什么更外层的作用域了。但是对于函数来说,它的locals和globals是不一样的。

并且我们还能看到一个有趣的现象,如果我们在foo下面再定义一个bar函数,那么在foo中是可以调用bar函数的,即使bar函数定义在foo的下面。因为在执行foo的时候,首先要执行模块,为模块创建一个栈帧,并且此时函数foo、bar都已经作为PyFunctionObject对象在模块对应的栈帧的f_locals(f_globals)里面了。而执行foo的时候,会抽出里面的PyCodeObject,然后创建栈帧。foo调用bar,但foo的f_locals里面没有bar,可f_globals里面是有的,因为这和模块的f_globals是一样的,所以是可以找到bar这个函数对应PyFunctionObject的,然后从里面抽出bar对应的PyCodeObject继续为其创建栈帧,执行。所以这和c语言有个很大的不同,c语言中函数是否可以调用是通过源代码中出现的位置定义的,而python则是基于运行时的命名空间决定的。而在执行foo之前,在为模块创建栈帧的时候,foo和bar都已经被包装成PyFunctionObject对象存在了模块的global(local)命名空间中了。

12.4 函数参数的实现

函数,最大的特点就是可以传入参数,否则就只能单纯的封装,这样未免太无趣了。对于python来说,参数会传什么对于函数来说是不知道的,函数体内部只是利用参数做一些事情,比如调用参数的get方法,但是到底能不能调用get方法,就取决于你给参数传的值到底是什么了。因此可以把参数看成是一个占位符,我们假设有这么个东西,直接把它当成已存在的变量或者常量去进行操作,然后调用的时候,将某个值传进去赋给相应的参数,然后参数对应着传入的具体的值将逻辑走一遍即可。

12.4.1 参数类别

在python中,函数的参数根据形式的不同可以分为四种类别

  • 位置参数(positional argument):foo(a, b),a和b便是位置参数
  • 关键字参数(keyword argument):foo(a, b, name="satori"),name便是关键字参数
  • 扩展位置参数(excess positional argument)foo(*agrs)
  • 扩展关键字参数(excess keyword argument):foo(**kwargs)

我们下面来看一下python的call_function是如何处理函数信息的。

//ceval.c
Py_LOCAL_INLINE(PyObject *) _Py_HOT_FUNCTION
call_function(PyObject ***pp_stack, Py_ssize_t oparg, PyObject *kwnames)
{	
    //获取PyFunctionObject对象
    PyObject **pfunc = (*pp_stack) - oparg - 1;
    PyObject *func = *pfunc;
    PyObject *x, *w;
    /*当python虚拟机在开始执行MAKE_FUNCTION指令时,会先获取一个指令参数oparg
    oparg里面记录函数的参数个数信息,包括位置参数和关键字参数的个数。
    虽然扩展位置参数和扩展关键字参数是更高级的用法,但是本质上也是由多个位置参数、多个关键字参数组成的。
    这就意味着,虽然python中存在四种参数,但是只要记录位置参数和关键字参数的个数,就能知道一共有多少个参数,进而知道一共需要多大的内存来维护参数。
    而且python的每个指令都是两个字节,第一个字节存放指令序列本身,第二个字节存放参数个数,既然是一个字节,说明最多只允许有255个参数,不过这已经足够了。
    */
    //nkwargs就是关键字参数的个数,nargs是位置参数的个数
    Py_ssize_t nkwargs = (kwnames == NULL) ? 0 : PyTuple_GET_SIZE(kwnames);
    Py_ssize_t nargs = oparg - nkwargs;
    PyObject **stack = (*pp_stack) - nargs - nkwargs;
    ...
    ...
}

另外还有一个co_nlocals和co_argcount。注意:从名字也能看出来这个不是PyFunctionObject里面的,而是PyCodeObject里面的。co_nlocals,我们之前说过,这是函数内部局部变量的个数,co_argcount是参数的个数。实际上,函数参数和函数局部变量是非常密切的,某种意义上函数参数就是一种函数局部变量,它们在内存中是连续放置的。当python需要为函数申请局部变量的内存空间时,就需要通过co_nlocals知道局部变量的总数,既然如此那还要co_argcount干什么呢?别急,看个例子

def foo(a, b, c, d=1):
    pass


print(foo.__code__.co_argcount)  # 4
print(foo.__code__.co_nlocals)  # 4


def foo(a, b, c, d=1):
    a = 1
    b = 1


print(foo.__code__.co_argcount)  # 4
print(foo.__code__.co_nlocals)  # 4


def foo(a, b, c, d=1):
    aa = 1


print(foo.__code__.co_argcount)  # 4
print(foo.__code__.co_nlocals)  # 5

函数的参数也是一个局部变量,因此co_nlocals是参数的个数加上函数体中新创建的局部变量的个数(注意是新创建的,比如参数有一个a,但是函数体里面的变量还是a,相当于重新赋值了,因此还是相当于一个参数),但是co_argcount则是存储记录参数的个数。因此一个很明显的结论:对于任意一个函数,co_nlocals至少是大于等于co_argcount的

def foo(a, b, c, d=1, *args, **kwargs):
    pass


print(foo.__code__.co_argcount)  # 4
print(foo.__code__.co_nlocals)  # 6

另外我们看到,对于扩展位置参数、扩展关键字参数来说,co_argcount是不算在内的,因为你完全可以不传递,因为直接当成0来算。而对于co_nlocals来说,我们在函数体内部肯定是拿到args和kwargs来说的,而这可以看成是两个参数。因此co_argcount是4,co_nlocals是6。其实所有的扩展位置参数是存在了一个PyTupleObject对象中的,所有的扩展关键字参数是存储在一个PyDictObject对象中的。而即使我们多传、或者不传,对于co_argcount和co_nlocals来说,都不会有任何改变了,因为这两者的值是在编译的时候就已经确定了的。

12.4.2 位置参数的传递

下面我们就来看看位置参数是如何传递的

a.py

def f(name, age):
    age = age + 5
    print(name, age)


age = 5
print(age)

f("satori", age)

对应字节码如下,直接先解释一下,当然到现在,这已经是很简单的了,对于很多人来说

加载字节码,谁的字节吗?显然是函数f对应的代码块
1           0 LOAD_CONST               0 (<code object f at 0x0000024C189C1030, file "f", line 1>)

		   将f这个符号load进来
            2 LOAD_CONST               1 (\'f\')
            
       	    将f的字节码包装成一个PyFunctionObject对象
            4 MAKE_FUNCTION            0
            
            将符号f和PyFunctionObject组成一个entry放入f_locals(f_globals)对应的PyDictObject里面
            6 STORE_NAME               0 (f)

此时跳到了第6行,load常量5,依旧是组成entry存储起来,由于这是在全局中,所以是STORE_NAME,而不是STORE_FAST
6           8 LOAD_CONST               2 (5)
           10 STORE_NAME               1 (age)

此时调用函数,当然是print函数,由于都是在全局中,所以是LOAD_NAME
7          12 LOAD_NAME                2 (print)
           14 LOAD_NAME                1 (age)
           
           CALL_FUNCTION调用print
           16 CALL_FUNCTION            1
           
           从栈顶将元素弹出来,并打印
           18 POP_TOP

此时开始了f函数的调用,调用之前肯定也要准备一下
于是从PyDictObject中将刚才存储的f对应的PyFunctionObject对象、以及"satori"这个字符串常量load进来
f和age是在全局变量中的,所以是LOAD_NAME,而"satori"字符串是一个常量,所以是LOAD_CONST
9          20 LOAD_NAME                0 (f)
           22 LOAD_CONST               3 (\'satori\')
           24 LOAD_NAME                1 (age)
           
           调用函数,此时跳到了源代码的第2行,因为遇见函数调用就会创建新的栈帧,并把代码执行的控制权交给新创建的栈帧,执行完了在返回,类似于递归,一层一层创建、执行,然后一层一层返回
           26 CALL_FUNCTION            2
           
           下面三条字节码的逻辑就无需解释了
           28 POP_TOP
           30 LOAD_CONST               4 (None)
           32 RETURN_VALUE

Disassembly of <code object f at 0x0000024C189C1030, file "f", line 1>:
我们说过参数age是一个局部变量,直接LOAD_FAST,5则是LOAD_CONST
2           0 LOAD_FAST                1 (age)
            2 LOAD_CONST               1 (5)
            
            加法运算
            4 BINARY_ADD
            
            此时的age函数体里面创建的局部变量,局部变量的存储是有优化的,所以是STORE_FAST
            6 STORE_FAST               1 (age)

下面则是LOAD_GLOBAL,会判断函数里面有没有定义print,但是显然没有,于是前往global、builtin命名空间里面去找,所以是LOAD_GLOBAL,而如果是在外层的话,则是LOAD_NAME
至于有没有LOAD_BUILTIN,实际是没有的,因为builtin是在global里面,我们通过global这个PyDictObject的__bultin__属性是可以找到builtin命名空间的、或者说是builtin对应的PyDictObject
3           8 LOAD_GLOBAL              0 (print)

		   而name和age则是里面的常量,则是LOAD_FAST
           10 LOAD_FAST                0 (name)
           12 LOAD_FAST                1 (age)
           
           调用函数
           14 CALL_FUNCTION            2
           
           从栈顶弹出元素,并打印
           16 POP_TOP
           
           返回默认为None,LOAD_CONST,向None、True、False这些关键字都是LOAD_CONST
           18 LOAD_CONST               0 (None)
           
           返回
           20 RETURN_VALUE

字节码虽然解释完了, 但是最重要的还是没有说。f(name, age),这里的name和age显然是外层定义的,但是外层定义的这两个变量是怎么传给函数f的。下面我们通过源码重新分析:

9          20 LOAD_NAME                0 (f)
           22 LOAD_CONST               3 (\'satori\')
           24 LOAD_NAME                1 (age)
           
           26 CALL_FUNCTION            2

我们注意到CALL_FUNCTION上面有三条指令,其实当这三条指令执行完毕之后,函数需要的参数已经被压入了运行时栈中。

通过_PyFunction_FastCallKeywords函数,然后执行function_code_fastcall

//call.c
static PyObject* _Py_HOT_FUNCTION
function_code_fastcall(PyCodeObject *co, PyObject *const *args, Py_ssize_t nargs,
                       PyObject *globals)
{
    PyFrameObject *f;
    PyThreadState *tstate = PyThreadState_GET();
    PyObject **fastlocals;
    Py_ssize_t i;
    PyObject *result;

    assert(globals != NULL);
    assert(tstate != NULL);
    //创建与函数对应的PyFrameObject,我们看到参数是co,所以是根据字节码指令来创建的
    f = _PyFrame_New_NoTrack(tstate, co, globals, NULL);
    if (f == NULL) {
        return NULL;
    }
	
    //关键:拷贝函数参数,从运行时栈到PyFrameObject.f_localsplus
    fastlocals = f->f_localsplus;
    ...
    return result;
}

从源码中我们看到通过_PyFrame_New_NoTrack创建了函数f对应的PyFrameObject对象,参数是f对应的PyFunctionObject对象中保存的PyCodeObject对象。随后,python虚拟机将参数逐个拷贝到新建的PyFrameObject对象的f_localsplus中。可在分析python虚拟机框架时,我们知道,这个f_localsplus所指向的内存块里面也存储了python虚拟机所使用的那个运行时栈。那么参数所占的内存和运行时栈所占的内存有什么关联呢?

//frameobject.c
//这个是_PyFrame_New_NoTrack,对外暴露的是PyFrame_New,但是本质上调用了这个
PyFrameObject* _Py_HOT_FUNCTION
_PyFrame_New_NoTrack(PyThreadState *tstate, PyCodeObject *code,
                     PyObject *globals, PyObject *locals)
{
    PyFrameObject *back = tstate->frame;
    PyFrameObject *f;
    PyObject *builtins;
    Py_ssize_t i;
    ...
    ...
        Py_ssize_t extras, ncells, nfrees;
        ncells = PyTuple_GET_SIZE(code->co_cellvars);
        nfrees = PyTuple_GET_SIZE(code->co_freevars);
        extras = code->co_stacksize + code->co_nlocals + ncells +
            nfrees;
        if (free_list == NULL) {
            //为f_localsplus申请extras的内存空间
            f = PyObject_GC_NewVar(PyFrameObject, &PyFrame_Type,
            extras);
		
        ...
        ...
        f->f_code = code;
        //获得f_localsplus中出去运行时栈,剩余的内存数
        extras = code->co_nlocals + ncells + nfrees;
        f->f_valuestack = f->f_localsplus + extras;
    ...
    ...
    f->f_lasti = -1;
    f->f_lineno = code->co_firstlineno;
    f->f_iblock = 0;
    f->f_executing = 0;
    f->f_gen = NULL;
    f->f_trace_opcodes = 0;
    f->f_trace_lines = 1;

    return f;
}

前面提到,在函数对应的PyCodeObject对象的co_nlocals域中,包含着函数参数的个数,因为函数参数也是局部符号的一种。所以从f_localsplus开始,extras中一定有供函数参数使用的内存。或者说,函数的参数存放在运行时栈之前的那段内存中。

另外从_PyFrame_New_NoTrack当中我们可以看到,在f_localsplus中存储函数参数的空间和运行时栈的空间在逻辑上是分离的,并不是共享同一片内存,尽管它们是连续的。这两者是鸡犬相闻,但又泾渭分明、老死不相往来。

在处理完参数之后,还没有进入PyEval_EvalFrameEx,所以此时运行时栈是空的。但是函数的参数已经位于f_localsplus中了。所以这时新建PyFrameObject对象的f_localsplus就是这样:

12.4.3 位置参数的访问

当参数拷贝的动作完成之后,就会进入新的PyEval_EvalFrameEx,开始真正的f的调用动作。

2           0 LOAD_FAST                1 (age)
            2 LOAD_CONST               1 (5)
            
            4 BINARY_ADD
            
            6 STORE_FAST               1 (age)

梦回指令集

LOAD_FAST:在函数里面load一个局部变量

LOAD_GLOBAL:在函数里面load一个全局变量、或者内置变量

LOAD_NAME:在外层模块中load一个变量

LOAD_CONST:不限范围,只要load的内容是一个常量

STORE_FAST:在函数里面创建一个局部变量

STORE_NAME:在外层模块中创建一个全局变量

STORE_GLOBAL:在外层模块或者函数里面创建一个被global声明的变量

首先对参数的读写,肯定是通过LOAD_FAST,LOAD_CONST,STORE_FAST这几条指令集完成的

//ceval.c
PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
{
    ...
    ...
    fastlocals = f->f_localsplus;
    ...
}

#define GETLOCAL(i)     (fastlocals[i])

[LOAD_FAST]
        TARGET(LOAD_FAST) {
            PyObject *value = GETLOCAL(oparg);
            if (value == NULL) {
                format_exc_check_arg(PyExc_UnboundLocalError,
                                     UNBOUNDLOCAL_ERROR_MSG,
                                     PyTuple_GetItem(co->co_varnames, oparg));
                goto error;
            }
            Py_INCREF(value);
            PUSH(value);
            FAST_DISPATCH();
        }

[STORE_FAST]
        PREDICTED(STORE_FAST);
        TARGET(STORE_FAST) {
            PyObject *value = POP();
            SETLOCAL(oparg, value);
            FAST_DISPATCH();
        }

所以我们发现,LOAD_FAST和STORE_FAST这一对指令是以f_localsplus这一片内存为操作目标的,指令0 LOAD_FAST 1 (age)的结果是将f_localsplus[1]对应的对象压入到运行时栈中,而我们刚才也看到f_localsplus[1]中存放的正是age。而在完成加法操作之后,又将结果通过STORE_FAST放入到f_localsplus[1]中,这样就实现了对a的更新,那么以后在print(a)的时候,得到的结果就是10了。

现在关于python的位置参数在函数调用时是如何传递的、在函数执行又是如何被访问,已经真相大白了。在调用函数时,python将函数参数的值从左至右依次压入到运行时栈中,而在call_function中通过调用_PyFunction_FastCallKeywords,进而调用function_code_fastcall,而在function_code_fastcall中,又将这些参数依次的拷贝到新建的与函数对应的PyFrameObject对象的f_localsplus中。最终的效果就是,python虚拟机将函数调用时使用的参数,从左至右依次地存放在新建的PyFrameObject对象的f_localsplus中。

因此在访问函数参数时,python虚拟机并没有按照通常访问符号的做法,去查什么命名空间,而是直接通过一个索引(偏移位置)来访问f_localsplus中存储的符号对应的值,是的,f_localsplus存储的是符号(变量名),并不是具体的值,所以python传参的方式都是引用传递,不会像golang一样,都是拷贝一份,否则那python效率就太低了,至于值是否改变,则取决于对应的值是可变对象还是不可变对象,而不是像其他编程语言那样通过传值或者传指针来决定是否改变。因此这种通过索引(偏移位置)来访问参数的方式也正是位置参数的由来。

12.4.4 位置参数的默认值

可能有人看到位置参数的默认值这几个字会感到懵逼,这难道不是关键字参数吗?其实位置参数、关键字参数一般是通过调用来体现的,而不是定义函数的时候。你在调用的时候,使用顺序将实参和形参进行对应的方式来传递参数时候,那么你传递的就是位置参数,如果是通过关键字的方式传递,那么传递的就是位置参数。比如:def foo(a, b=1),其中的a和b准确的说都是形参,或者说都是参数吧,但是由于b有了默认值,所以b也叫缺省参数或者默认参数。所以我们通过foo(2)调用时,并没有给b传值,但是定义的时候b=1,可以看做是默认给b传了一个1,即foo(2, 1),这个1就是默认值。所以位置参数、关键字参数是通过调用来体现的,而不是定义,也就是针对于实参的,是根据实参的传递方式来分类的,和形参是没有关系的。

下面就来考察一下默认值机制

a.py

def foo(a=1, b=2):
    print(a + b)


foo()
foo(b=3)
1           0 LOAD_CONST               7 ((1, 2))
            2 LOAD_CONST               2 (<code object foo at 0x0000015C4C5610E0, file "f", line 1>)
            4 LOAD_CONST               3 (\'foo\')
            6 MAKE_FUNCTION            1 (defaults)
            8 STORE_NAME               0 (foo)

5          10 LOAD_NAME                0 (foo)
           12 CALL_FUNCTION            0
           14 POP_TOP

6          16 LOAD_NAME                0 (foo)
           18 LOAD_CONST               4 (3)
           20 LOAD_CONST               5 ((\'b\',))
           22 CALL_FUNCTION_KW         1
           24 POP_TOP
           26 LOAD_CONST               6 (None)
           28 RETURN_VALUE

Disassembly of <code object foo at 0x0000015C4C5610E0, file "f", line 1>:
2           0 LOAD_GLOBAL              0 (print)
            2 LOAD_FAST                0 (a)
            4 LOAD_FAST                1 (b)
            6 BINARY_ADD
            8 CALL_FUNCTION            1
           10 POP_TOP
           12 LOAD_CONST               0 (None)
           14 RETURN_VALUE

我们对比一下开始的没有默认参数的函数,会发现相比于无默认参数的函数,有默认参数的函数,除了load函数体对应的PyCodeObject、和foo这个符号之外,会先将默认参数的值给load进来,将这三者都压入运行时栈。但是我们发现这是默认参数是组合成一个元组的形式入栈的,而且我们再来观察一下MAKE_FUNCTION这个指令,我们发现后面的参数是1 (defaults),之前的都是0,那么这个1是什么呢?而且又提示了我们一个defaults,我们知道PyFunctionObject对象有一个func_defaults,这两者之间有关系吗?那么带着这些疑问再来看看MAKE_FUNCTION指令。

[MAKE_FUNCTION]
        TARGET(MAKE_FUNCTION) {
        	//获取PyCodeObject、func_name,并创建PyFunctionObject
            PyObject *qualname = POP();
            PyObject *codeobj = POP();
            PyFunctionObject *func = (PyFunctionObject *)
                PyFunction_NewWithQualName(codeobj, f->f_globals, qualname);

            Py_DECREF(codeobj);
            Py_DECREF(qualname);
            if (func == NULL) {
                goto error;
            }
			
			//处理参数,这个是针对于闭包的
            if (oparg & 0x08) {
                assert(PyTuple_CheckExact(TOP()));
                func ->func_closure = POP();
            }
            //注解
            if (oparg & 0x04) {
                assert(PyDict_CheckExact(TOP()));
                func->func_annotations = POP();
            }
            
            //关键字默认参数,显然要定义在*后面的参数
            if (oparg & 0x02) {
                assert(PyDict_CheckExact(TOP()));
                func->func_kwdefaults = POP();
            }
            
            //默认关键字参数,我们发现确实是存储在func_defaults里面
            if (oparg & 0x01) {
                assert(PyTuple_CheckExact(TOP()));
                func->func_defaults = POP();
            }

            PUSH((PyObject *)func);
            DISPATCH();
        }

通过以上命令我们很容易看出,MAKE_FUNCTION指令除了创建PyFunctionObject对象,并且还会处理参数的默认值。MAKE_FUNCTION指令参数表示当前运行时栈中是存在默认值的,但是默认值具体多少个通过参数是看不到的,因为默认值都会按照顺序塞到一个PyTupleObject对象里面,所以整体相当于是一个。然后会调用PyFunction_SetDefaults将该PyTupleObject对象设置为PyFunctionObject.func_defaults的值,在python层面可以使用__defaults__访问。如此一来,函数参数的默认值也成为了PyFunctionObject对象的一部分,函数和其参数的默认值最终被python虚拟机捆绑在了一起,它和PyCodeObject、global命名空间一样,也被塞进了PyFunctionObject这个大包袱。所以说PyFunctionObject这个嫁衣做的是很彻底的,工具人PyFunctionObject对象,给个赞。

//functionobject.c
int
PyFunction_SetDefaults(PyObject *op, PyObject *defaults)
{	
    //两个参数,一个PyFunctionObject、一个PyTupleObject
    
    //检测机制,不用管
    if (!PyFunction_Check(op)) {
        PyErr_BadInternalCall();
        return -1;
    }
    if (defaults == Py_None)
        defaults = NULL;
    else if (defaults && PyTuple_Check(defaults)) {
        Py_INCREF(defaults);
    }
    else {
        PyErr_SetString(PyExc_SystemError, "non-tuple default args");
        return -1;
    }
    //设置
    Py_XSETREF(((PyFunctionObject *)op)->func_defaults, defaults);
    return 0;
}

函数的第一次调用:foo()

//call.c
//这个是通过ceval.c里面的call_function调用的
PyObject *
_PyFunction_FastCallKeywords(PyObject *func, PyObject *const *stack,
                             Py_ssize_t nargs, PyObject *kwnames)
{	
    //获取PyFunctionObject的PyCodeObject
    PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func);
    //获取PyFunctionObject的f_globals
    PyObject *globals = PyFunction_GET_GLOBALS(func);
    //看这里,这一步显然是获取PyFunctionObject里面的func_defaults
    PyObject *argdefs = PyFunction_GET_DEFAULTS(func);
    PyObject *kwdefs, *closure, *name, *qualname;
    PyObject **d;
    Py_ssize_t nkwargs = (kwnames == NULL) ? 0 : PyTuple_GET_SIZE(kwnames);
    Py_ssize_t nd;

    assert(PyFunction_Check(func));
    assert(nargs >= 0);
    assert(kwnames == NULL || PyTuple_CheckExact(kwnames));
    assert((nargs == 0 && nkwargs == 0) || stack != NULL);

    if (co->co_kwonlyargcount == 0 && nkwargs == 0 &&
        (co->co_flags & ~PyCF_MASK) == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE))
    {	
        //是否进入快速通道,首先要满足argdefs == NULL,但是我们发现*argdefs是有值的
        //所以argdefs这个指针是不为NULL的,因此进入通道失败。所以函数如果有默认参数,是不会进入快速通道的。
        if (argdefs == NULL && co->co_argcount == nargs) {
        //另外我们发现即使函数定义的时候没有默认参数,但是我们调用的时候通过关键字参数传参话,也不会进入快速通道
        //首先这里的nargs是通过call_function函数传递的,而这个nargs在call_function函数中是Py_ssize_t nargs = oparg - nkwargs;
        //所以这里的nargs就是传递的参数个数减去通过关键字参数方式传递的参数个数
        //而co_argcount是函数参数的总个数,所以一旦哪怕有一个参数使用了关键字参数的方式传递,都会造成两者不相等,从而无法进入快速通道
        //因此在CPython中,一个函数若想进入快速通道,只要满足以下两点即可
        /*
        1.函数定义的时候不可以有默认参数
        2.函数调用时,必须都通过位置参数指定。
        */
            return function_code_fastcall(co, stack, nargs, globals);
        }
        //但是这样的条件毕竟太苛刻了,毕竟参数哪能没有默认值呢?所以python还提供了一种进入快速通道的方式
        //我们发现在有默认的前提下,如果还能满足nargs==0 && co->co_argcount == PyTuple_GET_SIZE(argdefs)也能进入快速通道
        //co->co_argcount == PyTuple_GET_SIZE(argdefs)是要求函数的参数个数必须等于默认参数的个数,也就是函数参数全是默认参数
        //nargs==0则是需要传入的参数个数减去通过关键字参数传递的参数个数等于0,即要么不传参(都是用默认参数)、要么全部都通过关键字参数的方式传参。
        //这种方式也可以进入快速通道
        else if (nargs == 0 && argdefs != NULL
                 && co->co_argcount == PyTuple_GET_SIZE(argdefs)) {
            /* function called with no arguments, but all parameters have
               a default value: use default values as arguments .*/
            stack = &PyTuple_GET_ITEM(argdefs, 0);
            return function_code_fastcall(co, stack, PyTuple_GET_SIZE(argdefs),
                                          globals);
        }
    }
	
    //如果以上两点都无法满足的话,那么就没办法了,只能走常规方法了
    //这里是获取函数的一些属性,默认关键字参数、闭包等等
    kwdefs = PyFunction_GET_KW_DEFAULTS(func);
    closure = PyFunction_GET_CLOSURE(func);
    name = ((PyFunctionObject *)func) -> func_name;
    qualname = ((PyFunctionObject *)func) -> func_qualname;
	
    //这里则是获取默认参数的值的地址和默认参数的个数
    if (argdefs != NULL) {
        d = &PyTuple_GET_ITEM(argdefs, 0);
        nd = PyTuple_GET_SIZE(argdefs);
    }
    else {
        d = NULL;
        nd = 0;
    }

    return _PyEval_EvalCodeWithName((PyObject*)co, globals, (PyObject *)NULL,
                                    stack, nargs, //位置参数信息
                                    nkwargs ? &PyTuple_GET_ITEM(kwnames, 0) : NULL,
                                    stack + nargs,
                                    nkwargs, 1,//关键字参数信息
                                    d, (int)nd, kwdefs,//默认参数信息
                                    closure, name, qualname);
}

_PyEval_EvalCodeWithName是一个非常重要的函数,在后面分析扩展位置参数和扩展关键字参数是还会遇到。

PyObject *
_PyEval_EvalCodeWithName(PyObject *_co, PyObject *globals, PyObject *locals,
           PyObject *const *args, Py_ssize_t argcount,  //位置参数的信息
           PyObject *const *kwnames, PyObject *const *kwargs,//关键字参数的信息
           Py_ssize_t kwcount, int kwstep,
           PyObject *const *defs, Py_ssize_t defcount,//默认参数的信息
           PyObject *kwdefs, PyObject *closure,
           PyObject *name, PyObject *qualname)
{
    PyCodeObject* co = (PyCodeObject*)_co;
    PyFrameObject *f;
    PyObject *retval = NULL;
    PyObject **fastlocals, **freevars;
    PyThreadState *tstate;
    PyObject *x, *u;
    const Py_ssize_t total_args = co->co_argcount + co->co_kwonlyargcount;
    Py_ssize_t i, n;
    PyObject *kwdict;

    ...
    ...

以上是关于《python解释器源码剖析》第12章--python虚拟机中的函数机制的主要内容,如果未能解决你的问题,请参考以下文章

《python解释器源码剖析》第4章--python中的list对象

《python解释器源码剖析》第13章--python虚拟机中的类机制

《python解释器源码剖析》第10章--python虚拟机中的一般表达式

《python解释器源码剖析》第11章--python虚拟机中的控制流

《python解释器源码剖析》第17章--python的内存管理与垃圾回收

《python解释器源码剖析》第15章--python模块的动态加载机制