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

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

一、关于PyFunctionObject对象

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

二、无参函数的调用

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

def f():
    ...
f()
  • 对应的指令字节码如下:
  1           0 LOAD_CONST               0 (<code object f at 0x000001F28B9D8A50, file "demo.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

   consts
      code
  2           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE
  • 从上面的代码可以看出代码在经过编译后,产生了两个PyCodeObject对象,常规部分对应demo.py,code部分对应f()。
  • 按照顺序从上往下看,分别是:
  • 声明函数
 1           0 LOAD_CONST               0 (<code object f at 0x000001F28B9D8A50, file "demo.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
  • 实现函数
 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名字空间。
Objects\funcobject.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;
        }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容