Python内核阅读(十三): 函数对象和函数调用

Python 2017-08-15

起步

python中, 任何东西都是一个对象, 函数也不例外, 函数对象结构 PyFunctionObject :

[functionobject.h]
typedef struct {
    PyObject_HEAD
    PyObject *func_code;        // 编译后的 PyCodeObject 对象
    PyObject *func_globals;     // 运行时的global名字空间
    PyObject *func_defaults;    // 默认参数(tuple or null)
    PyObject *func_kwdefaults;  // 带声明的参数默认值 dict or null
    PyObject *func_closure;     // 闭包函数集合, null or tuple
    PyObject *func_doc;         // 函数的文档 PyStringObject
    PyObject *func_name;        // 函数名, __name__ 属性, 是PyStringObject对象
    PyObject *func_dict;        // 函数的 __dict__ 属性, PyDictObject 或 null
    PyObject *func_weakreflist; 
    PyObject *func_module;      // 函数的__module__ ,可以是任意对象
    PyObject *func_annotations; // 注释, null or dict
    PyObject *func_qualname;    /* The qualified name */
} PyFunctionObject;

在python中, 有两个对象都和函数有关, PyCodeObject和PyFunctionObject, 这两个对象的区别是, PyCodeObject对象是一段python代码的静态表示, 对一个code block会产生一个且只有一个PyCodeObject. 而PyFunctionObject则不同, PyFunctionObject是python代码运行时动态产生的, 它是在执行一个 def 语句时候创建的. 在PyFunctionObject中, 也会包含函数的静态信息, 这些信息保存在 func_code 中, 这个成员指向的是函数代码对应的PyCodeObject对象, 此外, PyFunctionObject还包含一些函数在执行时必要的动态信息, 即上下文, 如 func_globals, 就表明函数执行是关联了global名字空间. global 作用域中的符号和变量值必须在运行时才确定.

函数对象的创建

以无参函数调用入手, 因为不用去考虑复杂的参数传递机制的细节, 而将关注点放在函数调用的流程框架上:

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

其编译的code和字节码如下:

co_consts : (<code object f at 0x000001C5F7EFE660, file "compile.py", line 1>, 'f', None)
co_names : ('f',)
co_stacksize : 2
  1           0 LOAD_CONST               0 (<code object f at 0x000001C5F7EFE660, file "compile.py", line 1>)
              2 LOAD_CONST               1 ('f')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (f)

  3           8 LOAD_NAME                0 (f)
             10 CALL_FUNCTION            0
             12 POP_TOP
             14 LOAD_CONST               2 (None)
             16 RETURN_VALUE

这段代码的编译共产生了两个PyCodeObject对象, 一个是这个文件, 另一个是函数f():

TARGET(MAKE_FUNCTION) {
    PyObject *qualname = POP(); // 函数的名字
    PyObject *codeobj = POP();  // 函数对应的PyCodeObject对象
    PyFunctionObject *func = (PyFunctionObject *)
        PyFunction_NewWithQualName(codeobj, f->f_globals, qualname);

    Py_DECREF(codeobj);
    Py_DECREF(qualname);
    // 处理参数
    ...

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

在创建函数指令之前, 先通过LOAD_CONST将函数f对应的PyCodeObject对象压入运行时栈中. 然后以该对象和当前的PyFrameObject中的global名字空间, 通过 PyFunction_NewWithQualName 创建一个新的PyFunctionObject对象:

PyObject * PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname)
{
    PyFunctionObject *op;
    PyObject *doc, *consts, *module;
    static PyObject *__name__ = PyUnicode_InternFromString("__name__");

    // 创建PyFunctionObject对象
    op = PyObject_GC_New(PyFunctionObject, &PyFunction_Type);
    if (op == NULL)
        return NULL;

    op->func_weakreflist = NULL;
    Py_INCREF(code);
    op->func_code = code;   // 设置PyCodeObject对象
    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; // 函数中的文档

    ...

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

在创建PyFunctionObject对象之后, MAKE_FUNCTION还会进行一些处理函数的参数动作, MAKE_FUNCTION指令中, 新建的function对象通过PUSH操作压入到运行时栈中.

函数调用

函数调用从 CALL_FUNCTION 指令开始, 前面在介绍了 print 信息输出时也有涉及到函数, 当时没有详细介绍函数机制, 现在重新认识一下函数调用:

TARGET(CALL_FUNCTION) {
    PyObject **sp, *res;
    sp = stack_pointer;
    res = call_function(&sp, oparg, NULL);
    stack_pointer = sp;
    PUSH(res);
    if (res == NULL) {
        goto error;
    }
    DISPATCH();
}

这个指令中, 获得了当前运行时栈的栈顶指针后就调用 call_function 了:

[ceval.c]
Py_LOCAL_INLINE(PyObject *) _Py_HOT_FUNCTION call_function(PyObject ***pp_stack, Py_ssize_t oparg, PyObject *kwnames)
{
    PyObject **pfunc = (*pp_stack) - oparg - 1;
    PyObject *func = *pfunc;    // 获取PyFunctionObject 对象
    PyObject *x, *w;
    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.
    */
    if (PyCFunction_Check(func)) {
        PyThreadState *tstate = PyThreadState_GET();
        C_TRACE(x, _PyCFunction_FastCallKeywords(func, stack, nargs, kwnames));
    }
    else if (Py_TYPE(func) == &PyMethodDescr_Type) {
        ...
    }
    else {
        ...

        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));

    /* Clear the stack of the function object. */
    while ((*pp_stack) > pfunc) {
        w = EXT_POP(*pp_stack);
        Py_DECREF(w);
    }

    return x;
}

其中:

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

显然我们的函数创建对象时用的对象是 PyFunction_Type , 因此按照if路径走的是 x = _PyFunction_FastCallKeywords(func, stack, nargs, kwnames); . 分析调用前的变量的值都是啥, pp_stack 是在 CALL_FUNCTION 指令代码中传入的当时运行时栈的栈顶指针, oparg 是0 , 传进来的 kwnames 是NULL. 因此 pfunc = pp_stack - 1 , 所以 *pfunc 就是func, 即在MAKE_FUNCTION中创建的PyFunctionObject对象. 后面的 nkwargsnargs 显然都是0.

[call.c]
PyObject * _PyFunction_FastCallKeywords(PyObject *func, PyObject **stack,
                             Py_ssize_t nargs, PyObject *kwnames)
{
    PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func);   // 获得PyCodeObject对象
    PyObject *globals = PyFunction_GET_GLOBALS(func);               // 获得globals名字空间
    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;

    if (co->co_kwonlyargcount == 0 && nkwargs == 0 &&
        co->co_flags == (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)) {
            return function_code_fastcall(co, stack, PyTuple_GET_SIZE(argdefs),
                                          globals);
        }
    }

    ...
    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);
}

这段代码中, 包含了两条执行路径, 无参和有参的情况, f函数是无参, 显然执行了 return function_code_fastcall(co, stack, nargs, globals);.:

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

    // 创建新的PyFrameObject
    f = _PyFrame_New_NoTrack(tstate, co, globals, NULL);
    if (f == NULL) {
        return NULL;
    }

    fastlocals = f->f_localsplus;
    // 处理参数
    for (i = 0; i < nargs; i++) {
        Py_INCREF(*args);
        fastlocals[i] = *args++;
    }
    result = PyEval_EvalFrameEx(f,0);
    ...
    return result;
}

使用code对象和当前线程创建了一个新的PyFrameObject, 然后 PyEval_EvalFrameEx 执行栈帧, 也就是真正进入了所谓的函数调用状态, 在新的栈帧中执行代码,.

函数执行时的名字空间

在创建PyFunctionObject时,有一个参数globals, 这个globals最终成为函数f对象的PyFrameObject中global名字空间-- f_globals. 在那时说明 LOAD_NAME 指令时, 搜索顺序是:f_locals, f_globals, f_builtins. 这意味着, 当前的PyFrameObject对象中的与新建的PyFrameObject对象的global名字空间是同一个.

函数参数的实现

python中参数的形式很多样, 可以分为四种类型:

  • 位置参数: fun(a, b), a, b是位置参数
  • 键参数: fun(a, b, name='python'), 其中name就是键参数
  • 扩展位置参数: fun(a, b, *lst), 其中lst是扩展位置参数
  • 扩展键参数: fun(a, b, **kw), 其中kw是扩展键参数

扩展位置参数是允许在函数调用时调用参数的数量个数可变. 参数的处理过程需要回顾一下 call_function :

[ceval.c]
Py_LOCAL_INLINE(PyObject *) _Py_HOT_FUNCTION call_function(PyObject ***pp_stack, Py_ssize_t oparg, PyObject *kwnames)
{
    PyObject **pfunc = (*pp_stack) - oparg - 1;
    PyObject *func = *pfunc;    // 获得PyFunctionObject对象
    PyObject *x, *w;
    Py_ssize_t nkwargs = (kwnames == NULL) ? 0 : PyTuple_GET_SIZE(kwnames);// 键参数个数
    Py_ssize_t nargs = oparg - nkwargs;     // 位置参数个数
    PyObject **stack = (*pp_stack) - nargs - nkwargs;
    ...
}

虚拟机执行 CALL_FUNCTION 指令时, 首先获得一个指令参数oparg, 这个参数中表示了调用函数时需要的函数参数个数信息. python存在四种参数形式, 而则oparg只是个数字, 显然不能详细表示四种参数具体信息, 实际上它表示了一共有多少个参数.

python的每个指令都是占用两个字节, 第一个字节存指令, 第二字节存放参数个数, 这意味这oparg值最多255, 也就是说python中函数参数个数最多255个.

def f(a, b):
    print(a)
f(1, b=2)

函数调用时用:

8  LOAD_NAME                0 (f)
10 LOAD_CONST               2 (1)
12 LOAD_CONST               3 (2)
14 LOAD_CONST               4 (('b',))
16 CALL_FUNCTION_KW         2

当有键参数时, 调用了 CALL_FUNCTION_KW 这个实现和 CALL_FUNCTION 差不多:

TARGET(CALL_FUNCTION_KW) {
    PyObject **sp, *res, *names;
    names = POP();
    assert(PyTuple_CheckExact(names) && PyTuple_GET_SIZE(names) <= oparg);
    sp = stack_pointer;
    res = call_function(&sp, oparg, names);
    stack_pointer = sp;
    PUSH(res);
    ...
    DISPATCH();
}

只是在 call_function 中多个一个 names 参数, 在CALL_FUNCTION传的是NULL. 如果我们调用是 f(1, 2) 那么 (nkwargs, nargs) 的值就是 (0, 2), 而 f(1, b=2) 则是(1, 1). 可见 nkwargs, nargs 能反应真实位置参数和键参数的个数. 键参数是个tuple类型, 因此 oparg-len(nkwargs) 就是nargs的个数了.

位置参数的传递

调用f(1, 2) 是需要将1和2分别赋值给a和b的. 先不分析键参数, 来看看f函数对应的指令:

  2           0 LOAD_GLOBAL              0 (print)
              2 LOAD_FAST                0 (a)
              4 CALL_FUNCTION            1
              6 POP_TOP

  3           8 LOAD_GLOBAL              0 (print)
             10 LOAD_FAST                1 (b)
             12 CALL_FUNCTION            1
             14 POP_TOP
             16 LOAD_CONST               0 (None)
             18 RETURN_VALUE

参数的使用上用的是 LOAD_FAST :

TARGET(LOAD_FAST) {
    PyObject *value = GETLOCAL(oparg);
    Py_INCREF(value);
    PUSH(value);
    FAST_DISPATCH();
}

这个命令显然和 LOAD_NAME 区别仅仅是, LOAD_FAST只从locals名字空间获取, 而LOAD_NAME则会根据LGB规则顺序获取. 那么这些参数是怎么进入locals名字空间的呢, 也就是说在 call_function 的时候, 参数列表应该注入到f的栈帧对象的locals中:

static PyObject* _Py_HOT_FUNCTION function_code_fastcall(PyCodeObject *co, PyObject **args, Py_ssize_t nargs,
                       PyObject *globals)
{
    ...
    // 创建新的PyFrameObject
    f = _PyFrame_New_NoTrack(tstate, co, globals, NULL);
    fastlocals = f->f_localsplus;
    // 处理参数
    for (i = 0; i < nargs; i++) {
        fastlocals[i] = *args++;
    }
    ...
}

显而易见了.

位置参数的默认值

如果函数有默认值, 如:

def f(a=1, b=2):
    print(a)
    print(b)
f()

多余的指令就不贴了, 默认参数的处理在创建PyFunctionObject对象时候处理的:

0 LOAD_CONST               5 ((1, 2))
2 LOAD_CONST               2 (<code object f at 0x000001A51BCDE6F0, file "compile.py", line 1>)
4 LOAD_CONST               3 ('f')
6 MAKE_FUNCTION            1

在创建函数对象时, 默认参数值以tuple形式入栈. 回头再看看 MAKE_FUNCTION 命令:

TARGET(MAKE_FUNCTION) {
    PyObject *qualname = POP();
    PyObject *codeobj = POP();
    PyFunctionObject *func = (PyFunctionObject *) PyFunction_NewWithQualName(codeobj, f->f_globals, qualname);

    // 处理参数
    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();
}

默认参数存在了 func->func_defaults 中. 如果在CALL_FUNCTION指令中, 发现传递的位置参数个数小于fun对象中的参数, 那python需要为函数设定默认参数:

[ceval.c]
PyObject * _PyEval_EvalCodeWithName(/* 省略参数  */) {
    ...
    // 传递的参数个数小于fun对象定义的参数个数时
    if (argcount < co->co_argcount) {
        Py_ssize_t m = co->co_argcount - defcount;
        Py_ssize_t missing = 0;

        if (n > m)
            i = n - m;
        else
            i = 0;
        for (; i < defcount; i++) {
            // 赋值默认值
            if (GETLOCAL(m+i) == NULL) {
                PyObject *def = defs[i];
                Py_INCREF(def);
                SETLOCAL(m+i, def);
            }
        }
    }
    ...
}

这段代码的作用也就是如果函数申明是def f(a=1, b=2) 而调用时 f(3) 那么就把2赋值给locals空间的b中.

扩展位置参数和扩展键参数

前面已经有看到扩展位置参数其实是个tuple, 因此可以猜想扩展键参数其实就是个dict对象.如果我们声明一个函数如下:

def f(value, *lst, **keys):
    pass
f(-1, 1, 2, 3, a=4, b=5)

对应的code和指令为:

co_consts : (<code object f at 0x00000238A685E6F0, file "compile.py", line 1>, 'f', 1, 2, 3, 4, 5, ('a', 'b'), None, -1)
co_names : ('f',)
co_nlocals : 0
co_stacksize : 8
co_varnames : ()
  1           0 LOAD_CONST               0 (<code object f at 0x00000238A685E6F0, file "compile.py", line 1>)
              2 LOAD_CONST               1 ('f')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (f)

  3           8 LOAD_NAME                0 (f)
             10 LOAD_CONST               9 (-1)
             12 LOAD_CONST               2 (1)
             14 LOAD_CONST               3 (2)
             16 LOAD_CONST               4 (3)
             18 LOAD_CONST               5 (4)
             20 LOAD_CONST               6 (5)
             22 LOAD_CONST               7 (('a', 'b'))
             24 CALL_FUNCTION_KW         6
             26 POP_TOP
             28 LOAD_CONST               8 (None)
             30 RETURN_VALUE

当含有键参数时采用了 CALL_FUNCTION_KW 指令, 前面说了, 这个指令和CALL_FUNCTION就是多了传了键参数的键组成了tuple.

PyObject * _PyEval_EvalCodeWithName(/* 省略参数  */) {
    ...
    // 先设置键参数默认值, 这是在函数声明时用的默认值
    if (co->co_kwonlyargcount > 0) {
        Py_ssize_t missing = 0;
        for (i = co->co_argcount; i < total_args; i++) {
            PyObject *name;
            if (GETLOCAL(i) != NULL)
                continue;
            name = PyTuple_GET_ITEM(co->co_varnames, i);
            if (kwdefs != NULL) {
                PyObject *def = PyDict_GetItem(kwdefs, name);
                if (def) {
                    Py_INCREF(def);
                    SETLOCAL(i, def);
                    continue;
                }
            }
            missing++;
        }
    }

    // 处理扩展参数位置
    kwcount *= kwstep;
    for (i = 0; i < kwcount; i += kwstep) {
        PyObject **co_varnames;
        PyObject *keyword = kwnames[i];
        PyObject *value = kwargs[i];
        Py_ssize_t j;
        ...
        if (PyDict_SetItem(kwdict, keyword, value) == -1) {
            goto fail;
        }
        continue;
    }
    ...
}

python在编译一个函数时, 如果在其形式参数中有*lst这样的扩展位置参数形式或 **keys 时, 那么python会在编译得到的PyCodeObject对象中co_flags中添加标识.


本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

赏个馒头吧