大师兄的Python源码学习笔记(十九): 虚拟机中的函数机制(一)

时间:2021-6-9 作者:qvyue

大师兄的Python源码学习笔记(十八): 虚拟机中的控制流(五)

一、关于PyFunctionObject对象

  • 在Python中,PyCodeObject对象是一段Python源代码的静态表示,在源代码经过编译后,一个Code Block会产生一个且只有一个PyCodeObject,它包含了这个Code Block的静态信息。
  • PyFunctionObject对象则是在Python代码运行时动态产生的(执行def语句时)。
  • PyFunctionObject对象中会包含这个函数的静态信息(*func_code中),此外还会包含一些函数在执行时必须的动态信息(上下文信息)。
Includefuncobject.h

typedef struct {
    PyObject_HEAD
    PyObject *func_code;        /* A code object, the __code__ attribute */
    PyObject *func_globals;     /* A dictionary (other mappings won't do) */
    PyObject *func_defaults;    /* NULL or a tuple */
    PyObject *func_kwdefaults;  /* NULL or a dict */
    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 */
    PyObject *func_annotations; /* Annotations, a dict or NULL */
    PyObject *func_qualname;    /* The qualified name */

    /* 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;
成员 说明
*func_code 对应函数编译后的PyCodeObject对象
*func_globals 函数运行时的global名字空间
*func_defaults 默认参数(tuple)
*func_kwdefaults 默认参数(dict)
*func_closure 用于实现闭包的cell对象元祖
*func_doc 函数的文档
*func_name 函数名(__name__属性)
*func_dict 函数的__dict__属性
*func_weakreflist 弱引用
*func_module 函数的__module__
*func_annotations 函数的注释
*func_qualname 函数的qualified name
  • 对于一段代码,其对应的PyCodeObject对象只有一个,而对应的PyFunctionObject对象却可能有很多个,每次调用函数都会创建PyFunctionObject对象。
  • 而所有PyFunctionObject对象都会关联到PyCodeObject对象。
    大师兄的Python源码学习笔记(十九): 虚拟机中的函数机制(一)

二、无参函数的调用

1.1 函数对象的创建
  • 无参函数调用是最简单的函数调用形式,创建一个简单的案例:
demo.py

def f():
    ...
f()
  • 对应的指令字节码如下:
  1           0 LOAD_CONST               0 ()
              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

   consts
      code
  2           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE
  • 从上面的代码可以看出代码在经过编译后,产生了两个PyCodeObject对象,常规部分对应demo.py,code部分对应f()。
  • 按照顺序从上往下看,分别是:
  • 声明函数
 1           0 LOAD_CONST               0 ()
             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
  • 实现函数
 2           0 LOAD_CONST               0 (None)
             2 RETURN_VALUE
  • 从上面的顺序可以看出,声明函数实现函数调用函数分割成两个不同的PyCodeObject,而在Python代码中,他们应该是一个完整的整体。
  • 这是因为在Python中函数也是对象,在调用函数之前必须先创建这个函数对象,而这里的创建工作是通过def f()这条代码完成的,他对应了字节码指令Make_FUNCTION
ceval.c

        TARGET(MAKE_FUNCTION) {
            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();
            }
            if (oparg & 0x01) {
                assert(PyTuple_CheckExact(TOP()));
                func->func_defaults = POP();
            }

            PUSH((PyObject *)func);
            DISPATCH();
        }
  • 在这里,虚拟机将f对应的qualnamePyCodeObject对象弹出,与当前PyFrameObject对象维护的global名字空间f_globals对象为参数,通过PyFunction_NewWithQualName创建了一个新的PyFunctionObject,而这个f_globals将成为函数f在运行时的global名字空间。
Objectsfuncobject.c

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

    if (__name__ == NULL) {
        __name__ = PyUnicode_InternFromString("__name__");
        if (__name__ == NULL)
            return NULL;
    }

    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__: If module name is in globals, use it.
       Otherwise, use None. */
    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还会进行一些处理函数参数的动作,并压入运行时栈中,这里由于是无参函数暂时跳过。
1.2 函数调用
  • 函数调用从CALL_FUNCTION指令开始:
ceval.c

        PREDICTED(CALL_FUNCTION);
        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;
    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) {
        PyThreadState *tstate = PyThreadState_GET();
        if (nargs > 0 && tstate->use_tracing) {
            /* We need to create a temporary bound method as argument
               for profiling.

               If nargs == 0, then this cannot work because we have no
               "self". In any case, the call itself would raise
               TypeError (foo needs an argument), so we just skip
               profiling. */
            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) {
            /* Optimize access to bound methods. Reuse the Python stack
               to pass 'self' as the first argument, replace 'func'
               with 'self'. It avoids the creation of a new temporary tuple
               for arguments (to replace func with self) when the method uses
               FASTCALL. */
            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);
        }

        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;
}
  • 在上面代码中,首先会通过PyCFunction_Check判断是不是C函数,如果是则进入快车道。
  • 如果是普通函数,则通过PyFunction_Check检查后,会进入分支_PyFunction_FastCallKeywords_PyObject_FastCallKeywords
  • 但不管进入哪个分支,最终虚拟机会调用PyEval_EvalFrameEx
ceval.c

PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
    PyThreadState *tstate = PyThreadState_GET();
    return tstate->interp->eval_frame(f, throwflag);
}
  • PyEval_EvalFrameEx开始,虚拟机进入了函数调用状态:创建新的帧栈->在新的帧栈中执行代码。
  • 在最终通过PyEval_EvalFrameEx时,PyFunctionObject对象的影响已经消失,真正对新帧栈产生影响的是在PyFunctionObject中存储的PyCodeObject对象和global名字空间。
1.3 函数执行时的名字空间
  • 在执行demo.py时的字节码指令和执行f的字节码指令时的名字空间是同一个名字空间。
  • 这表示在函数f中可以直接使用f函数外的符号。
  • 此外在字节码指令STORENAME处,会将符号f的放入f_local中,也就是同时将f放进f_globals中,得以在函数f中实现递归。
ceval.c

        case STORE_NAME:
        {
            PyObject *names = f->f_code->co_names;
            PyObject *name = GETITEM(names, oparg);
            PyObject *locals = f->f_locals;
            if (locals && PyDict_CheckExact(locals) &&
                PyDict_GetItem(locals, name) == v) {
                if (PyDict_DelItem(locals, name) != 0) {
                    PyErr_Clear();
                }
            }
            break;
        }
声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。