美文网首页
【Python】虚拟机中的函数机制

【Python】虚拟机中的函数机制

作者: lndyzwdxhs | 来源:发表于2018-12-10 14:21 被阅读22次

0x01 PyFunctionObject对象

typedef struct {
    PyObject_HEAD
    PyObject *func_code;    /* 对应函数编译后的PyCodeObject对象 A code object */
    PyObject *func_globals; /* 函数运行时的global名字空间 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;     /* 函数的文档(PyStringObject对象) The __doc__ attribute, can be anything */
    PyObject *func_name;    /* 函数的名称,函数的__name__属性 The __name__ attribute, a string object */
    PyObject *func_dict;    /* 函数的__dict__属性 The __dict__ attribute, a dict or NULL */
    PyObject *func_weakreflist; /* List of weak references */
    PyObject *func_module;  /* 函数的__module__属性 The __module__ attribute, can be anything */
} PyFunctionObject;
  • PyFunctionObject对象是Python代码在运行时动态产生的,准确的说是在执行了一个def语句产生的。
  • 函数的所有静态信息保存在PyFunctionObject.func_code中,其实它就是一个PyCodeObject对象,也就是函数编译后的结果。
  • PyFunctionObject对象中含包含了一些函数在执行时必须的动态信息(上下文信息):
    • func_globals:函数在执行时关联的global作用域
    • ......
  • 一段函数代码对应的PyCodeObject对象只有一个,但是对应的PyFunctionObject对象可能有多个(一个函数多次调用)

0x02 无参函数调用

##########################
def f():
    print "sssssssssss"
f()
##########################

# def f()对应的字节码
  1           0 LOAD_CONST               0 (<code object f at 00000000041A6CB0, file "test3.py", line 1>)
              3 MAKE_FUNCTION            0
              6 STORE_NAME               0 (f)
    # print "sssssssssss"对应的字节码
      2       0 LOAD_CONST               1 ('sssssssssss')
              3 PRINT_ITEM
              4 PRINT_NEWLINE
              5 LOAD_CONST               0 (None)
              8 RETURN_VALUE

# f()对应的字节码
  4           9 LOAD_NAME                0 (f)
             12 CALL_FUNCTION            0
             15 POP_TOP
             16 LOAD_CONST               1 (None)
             19 RETURN_VALUE

函数对象创建

Python虚拟机在遇到def f()语句时,会创建一个PyFunctionObject对象,是在MAKE_FUNCTION字节码指令中实现的。

PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
    for(;;){
        ......
        case MAKE_FUNCTION:
            // 获得与函数f对应的PyCodeObject对象
            v = POP(); /* code object */
            x = PyFunction_New(v, f->f_globals);
            Py_DECREF(v);
            /* XXX Maybe this should be a separate opcode? */
            if (x != NULL && oparg > 0) {
                v = PyTuple_New(oparg);
                if (v == NULL) {
                    Py_DECREF(x);
                    x = NULL;
                    break;
                }
                while (--oparg >= 0) {
                    w = POP();
                    PyTuple_SET_ITEM(v, oparg, w);
                }
                err = PyFunction_SetDefaults(x, v);
                Py_DECREF(v);
            }
            PUSH(x);
            break;
        ......
    }
}
  • MAKE_FUNCTION指令之前,LOAD_CONST指令会将函数代码块编译成的PyCodeObject对象压入到运行时栈中,MAKE_FUNCTIONPOP()出该对象,然后以该对象和当前PyFrameObject对象中的global名字空间f_globals作为参数,通过PyFunction_New()函数创建一个新的PyFunctionObject对象,这个f_globals将作为函数运行期间的global名字空间。
PyObject *
PyFunction_New(PyObject *code, PyObject *globals)
{
        // 申请PyFunctionObject对象所需的内存空间
    PyFunctionObject *op = PyObject_GC_New(PyFunctionObject,
                        &PyFunction_Type);
    static PyObject *__name__ = 0;
    if (op != NULL) {
                // 初始化PyFunctionObject对象中各个域
        PyObject *doc;
        PyObject *consts;
        PyObject *module;
        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_closure = NULL;
        consts = ((PyCodeObject *)code)->co_consts;
        if (PyTuple_Size(consts) >= 1) {
            doc = PyTuple_GetItem(consts, 0);
            if (!PyString_Check(doc) && !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;

        if (!__name__) {
            __name__ = PyString_InternFromString("__name__");
            if (!__name__) {
                Py_DECREF(op);
                return NULL;
            }
        }
        module = PyDict_GetItem(globals, __name__);
        if (module) {
            Py_INCREF(module);
            op->func_module = module;
        }
    }
    else
        return NULL;
    _PyObject_GC_TRACK(op);
    return (PyObject *)op;
}

函数调用

CALL_FUNCTION字节码指令就是函数的调用指令。

PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
    for(;;){
        ......
        case CALL_FUNCTION:
        {
            PyObject **sp;
            PCALL(PCALL_ALL);
            sp = stack_pointer;
                        x = call_function(&sp, oparg);
            stack_pointer = sp;
            PUSH(x);
            if (x != NULL)
                continue;
            break;
        }
        ......
    }
}

Python虚拟机在执行CALL_FUNCTION字节码指令的时候,获取了运行时栈的栈顶指针,然后就交给call_function()函数处理了。

static PyObject *
call_function(PyObject ***pp_stack, int oparg)
{
        // 处理函数参数信息
    int na = oparg & 0xff;
    int nk = (oparg>>8) & 0xff;
    int n = na + 2 * nk;
        // 获得PyFunctionObject对象
    PyObject **pfunc = (*pp_stack) - n - 1;
    PyObject *func = *pfunc;
    PyObject *x, *w;

    /* Always dispatch PyCFunction first, because these are
       presumed to be the most frequent callable object.
    */
    if (PyCFunction_Check(func) && nk == 0) {
        int flags = PyCFunction_GET_FLAGS(func);
        PyThreadState *tstate = PyThreadState_GET();

        PCALL(PCALL_CFUNCTION);
        if (flags & (METH_NOARGS | METH_O)) {
            PyCFunction meth = PyCFunction_GET_FUNCTION(func);
            PyObject *self = PyCFunction_GET_SELF(func);
            if (flags & METH_NOARGS && na == 0) {
                C_TRACE(x, (*meth)(self,NULL));
            }
            else if (flags & METH_O && na == 1) {
                PyObject *arg = EXT_POP(*pp_stack);
                C_TRACE(x, (*meth)(self,arg));
                Py_DECREF(arg);
            }
            else {
                err_args(func, flags, na);
                x = NULL;
            }
        }
        else {
            PyObject *callargs;
            callargs = load_args(pp_stack, na);
            READ_TIMESTAMP(*pintr0);
            C_TRACE(x, PyCFunction_Call(func,callargs,NULL));
            READ_TIMESTAMP(*pintr1);
            Py_XDECREF(callargs);
        }
    } else {
        if (PyMethod_Check(func) && PyMethod_GET_SELF(func) != NULL) {
            /* optimize access to bound methods */
            PyObject *self = PyMethod_GET_SELF(func);
            PCALL(PCALL_METHOD);
            PCALL(PCALL_BOUND_METHOD);
            Py_INCREF(self);
            func = PyMethod_GET_FUNCTION(func);
            Py_INCREF(func);
            Py_DECREF(*pfunc);
            *pfunc = self;
            na++;
            n++;
        } else
            Py_INCREF(func);
        READ_TIMESTAMP(*pintr0);
                // 对PyFunctionObject对象进行调用
        if (PyFunction_Check(func))
            x = fast_function(func, pp_stack, n, na, nk);
        else
            x = do_call(func, pp_stack, na, nk);
        READ_TIMESTAMP(*pintr1);
        Py_DECREF(func);
    }

    while ((*pp_stack) > pfunc) {
        w = EXT_POP(*pp_stack);
        Py_DECREF(w);
        PCALL(PCALL_POP);
    }
    return x;
}
  • 变量n表示的是运行时栈中,栈顶的多少元素是与参数相关的
  • 运行时栈中的函数指针通过(*pp_stack) - (na + 2 * nk) - 1拿到,pp_stack是栈顶指针,减去所有参数的偏移,再减1,就拿到了函数指针
  • 上面代码中PyFunction_Check(func)检查后,就会进入fast_function()函数中。
static PyObject *
fast_function(PyObject *func, PyObject ***pp_stack, int n, int na, int nk)
{
    PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func);
    PyObject *globals = PyFunction_GET_GLOBALS(func);
    PyObject *argdefs = PyFunction_GET_DEFAULTS(func);
    PyObject **d = NULL;
    int nd = 0;

    PCALL(PCALL_FUNCTION);
    PCALL(PCALL_FAST_FUNCTION);
        // 一般函数的快速通道
    if (argdefs == NULL && co->co_argcount == n && nk==0 &&
        co->co_flags == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE)) {
        PyFrameObject *f;
        PyObject *retval = NULL;
        PyThreadState *tstate = PyThreadState_GET();
        PyObject **fastlocals, **stack;
        int i;

        PCALL(PCALL_FASTER_FUNCTION);
        assert(globals != NULL);
        /* XXX Perhaps we should create a specialized
           PyFrame_New() that doesn't take locals, but does
           take builtins without sanity checking them.
        */
        assert(tstate != NULL);
        f = PyFrame_New(tstate, co, globals, NULL);
        if (f == NULL)
            return NULL;

        fastlocals = f->f_localsplus;
        stack = (*pp_stack) - n;

        for (i = 0; i < n; i++) {
            Py_INCREF(*stack);
            fastlocals[i] = *stack++;
        }
        retval = PyEval_EvalFrameEx(f,0);
        ++tstate->recursion_depth;
        Py_DECREF(f);
        --tstate->recursion_depth;
        return retval;
    }
    if (argdefs != NULL) {
        d = &PyTuple_GET_ITEM(argdefs, 0);
        nd = ((PyTupleObject *)argdefs)->ob_size;
    }
    return PyEval_EvalCodeEx(co, globals,
                 (PyObject *)NULL, (*pp_stack)-n, na,
                 (*pp_stack)-2*nk, nk, d, nd,
                 PyFunction_GET_CLOSURE(func));
}
  • 进入fast_function()函数以后,先取出PyFunctionObject对象中保存的PyCodeObject对象和函数运行时的global名字空间等信息。
  • 然后根据是“参数形式”分成两个分支:
    • 无参数的情况下会进入一般函数的通道,然后Python虚拟机会创建新的PyFrameObject对象,然后调用PyEval_EvalFrameEx伪CPU)函数执行
    • 另一条路径会进入PyEval_EvalCodeEx()函数,最终还是会调用到PyEval_EvalFrameEx伪CPU)中
  • PyEval_EvalFrameEx伪CPU)开始,才算是真正进行了“函数调用”的状态(实际上就是对x86机器函数调用的模拟:创建新的栈帧,在新的栈帧中执行代码。)。
  • 在最终执行PyEval_EvalFrameEx伪CPU)函数时,PyFrameObject对象已经失去了作用,它的作用只是将PyCodeObject对象和函数需要的global名字空间打包输送过来,然后叫交给PyFrameObject(栈帧)对象。

0x03 函数执行时的名字空间

函数的PyFrameObject(栈帧)中的global名字空间就是在调用CALL_FUNCTION字节码指令时,调用PyFunction_New()时创建PyFunctionObject对象时传入的参数,这个global参数其实就是当前PyFrameObject(栈帧)的global名字空间。

因为有了这个机制,所以在函数内部就可以调用函数外的变量了。

0x04 函数参数的实现

参数类别

f(a, b, *list, name='python', **keys)

  • 位置参数(positional argument):ab
  • 键参数(key argument):name='python'
  • 扩展位置参数(excess positional argument):*list
  • 扩展键参数(excess key argument):**keys
static PyObject *
call_function(PyObject ***pp_stack, int oparg)
{
    // 处理函数参数信息
    int na = oparg & 0xff;
    int nk = (oparg>>8) & 0xff;
    int n = na + 2 * nk;
    // 获得PyFunctionObject对象
    PyObject **pfunc = (*pp_stack) - n - 1;
    PyObject *func = *pfunc;
    PyObject *x, *w;
    ......
    return x;
}

在执行CALL_FUNCTION字节码指令时,会获得一个指令参数oparg,这个参数记录的是函数参数的个数(位置参数和键参数的个数)。

CALL_FUNCTION字节码指令参数的长度是2个字节,低字节记录的是位置参数的个数,高字节记录着键参数的个数。因此理论上,Python中函数参数只能有256个位置参数和256个键参数。

在上面的call_function代码中,na就是位置参数的个数,nk就是键参数的个数。

函数对应的PyCodeObject对象的中co_nlocals表示局部变量的个数,co_argcount表示函数一共有多少参数。

参数的运行时信息

上图可以看出,函数参数是位置参数还是键参数是由函数的实参形式决定的,而与函数定义时的形参没有任何关系。

1和例2中的n为什么不一样?因为n表示运行时栈中,函数指针距离栈顶的距离,键参数会会将keyvalue一起压入运行时栈,所以计算公式为:n = na + 2 * nk

3中的co_argcount=2co_nlocals=3,因为函数内部将*list作为了一个局部变量,所以函数参数个数co_argcount=2,局部变量co_nlocals=3。函数内部将list作为一个PyListObject对象来保存扩展位置参数。

4同例3

位置参数的传递

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

# def f(name, age):
1           0 LOAD_CONST               0 (<code object f at 00000000041EE330, file "test3.py", line 1>)
            3 MAKE_FUNCTION            0
            6 STORE_NAME               0 (f)
    # age += 5
    2           0 LOAD_FAST                1 (age)
                3 LOAD_CONST               1 (5)
                6 INPLACE_ADD
                7 STORE_FAST               1 (age)
    # print name, age
    3          10 LOAD_FAST                0 (name)
               13 PRINT_ITEM
               14 LOAD_FAST                1 (age)
               17 PRINT_ITEM
               18 PRINT_NEWLINE
               19 LOAD_CONST               0 (None)
               22 RETURN_VALUE

# age = 5
5           9 LOAD_CONST               1 (5)
           12 STORE_NAME               1 (age)
# print age
6          15 LOAD_NAME                1 (age)
           18 PRINT_ITEM
           19 PRINT_NEWLINE
# f("cs", age)
8          20 LOAD_NAME                0 (f)
           23 LOAD_CONST               2 ('cs')
           26 LOAD_NAME                1 (age)
           29 CALL_FUNCTION            2
           32 POP_TOP
# print age
10          33 LOAD_NAME                1 (age)
            36 PRINT_ITEM
            37 PRINT_NEWLINE
            38 LOAD_CONST               3 (None)
            41 RETURN_VALUE
  • 与无参函数不同的是,上面例子中在CALL_FUNCTION字节码指令之前多了3LOAD指令。可看出,函数需要的参数也被压入运行时栈中。接下来CALL_FUNCTION字节码指令的参数oparg2
static PyObject *
fast_function(PyObject *func, PyObject ***pp_stack, int n, int na, int nk)
{
        ......
        // 创建当前函数的栈帧
        f = PyFrame_New(tstate, co, globals, NULL);
        if (f == NULL)
            return NULL;
        
        fastlocals = f->f_localsplus;
        stack = (*pp_stack) - n;
        // 将参数拷贝到栈帧的f_localsplus域
        for (i = 0; i < n; i++) {
            Py_INCREF(*stack);
            fastlocals[i] = *stack++;
        }
        ......
}
  • 创建完新的栈帧后,然后将运行时栈的内的函数参数拷贝到f_localsplus域中。
  • 回忆之前的执行环境(PyFrameObject),函数参数使用的内存在f_localsplus中除了“运行时栈”以外的空间中。所以在还没有开始执行代码(PyEval_EvalFrameEx),函数的参数已经维护在栈帧中了。
    函数PyFrameObject栈帧对象

位置参数的访问

接下来就开始使用伪CPUPyEval_EvalFrameEx())来执行字节码指令。

#define GETLOCAL(i) (fastlocals[i])
#define SETLOCAL(i, value)  do { PyObject *tmp = GETLOCAL(i); \
                     GETLOCAL(i) = value; \
                                     Py_XDECREF(tmp); } while (0)

PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
    for(;;){
        ......
        case LOAD_FAST:
            x = GETLOCAL(oparg);
            if (x != NULL) {
                Py_INCREF(x);
                PUSH(x);
                goto fast_next_opcode;
            }
            format_exc_check_arg(PyExc_UnboundLocalError,
                UNBOUNDLOCAL_ERROR_MSG,
                PyTuple_GetItem(co->co_varnames, oparg));
            break;
        case STORE_FAST:
            v = POP();
            SETLOCAL(oparg, v);
            goto fast_next_opcode;
        ......
    }
}
  • LOAD_FASTSTORE_FAST字节码指令都是对f_localsplus的操作。

总结(函数参数的传递和访问):

  • 在进行函数调用(CALL_FUNCTION)之前,Python将函数参数从左到右压入运行时栈,然后在fast_function()中,又将这些参数拷贝到与函数对应的PyFrameObject(栈帧)中的f_localsplus域中。
  • 访问函数参数时,Python没有去查名字空间,而是直接通过索引来访问f_localsplus中存储的符号对应的值。这种通过索引进行访问的方法也正是“位置参数”名称的由来。
    函数调用过程中参数的变化序列

位置参数的默认值

带默认参数值的函数,def语句编译后的字节码指令如下所示:

# def f(name='yl', age=18):
1             0 LOAD_CONST               0 ('yl')
              3 LOAD_CONST               1 ('18')
              6 LOAD_CONST               2 (<code object f at 00000000041EEEB0, file "test3.py", line 1>)
              9 MAKE_FUNCTION            2
             12 STORE_NAME               0 (f)

然后执行MAKE_FUNCTION字节码指令的时候,参数为2(无参函数或参数无默认值的函数执行MAKE_FUNCTION指令时参数为0

PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
    for(;;){
        ......
        case MAKE_FUNCTION:
            // 获得与函数f对应的PyCodeObject对象
            v = POP(); /* code object */
            // 创建PyFunctionObject对象
            x = PyFunction_New(v, f->f_globals);
            Py_DECREF(v);
            // 处理函数参数默认值
            if (x != NULL && oparg > 0) {
                v = PyTuple_New(oparg);
                if (v == NULL) {
                    Py_DECREF(x);
                    x = NULL;
                    break;
                }
                while (--oparg >= 0) {
                    w = POP();
                    PyTuple_SET_ITEM(v, oparg, w);
                }
                err = PyFunction_SetDefaults(x, v);
                Py_DECREF(v);
            }
            PUSH(x);
            break;
        ......
    }
}
  • 创建完PyFunctionObject对象以后,会将所有默认值从运行时栈中弹出,装到PyTupleObject对象中,然后将PyTupleObject对象通过PyFunction_SetDefaults()函数,设置成PyFunctionObject对象的func_defaults域的值。
  • 这样PyCodeObject对象、global名字空间和函数参数默认值func_defaults都被PyFunctionObject对象携带捆绑在一起。
f()方式调用
static PyObject *
fast_function(PyObject *func, PyObject ***pp_stack, int n, int na, int nk)
{
    PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func);
    PyObject *globals = PyFunction_GET_GLOBALS(func);
        // 获取函数对应的PyFunctionObject对象中的func_defaults
    PyObject *argdefs = PyFunction_GET_DEFAULTS(func);
    PyObject **d = NULL;
    int nd = 0;

    PCALL(PCALL_FUNCTION);
    PCALL(PCALL_FAST_FUNCTION);
    ......
    if (argdefs != NULL) {
                // 获得函数参数默认值信息(第一个默认值的地址;默认值的个数)
        d = &PyTuple_GET_ITEM(argdefs, 0);
        nd = ((PyTupleObject *)argdefs)->ob_size;
    }
    return PyEval_EvalCodeEx(co, globals,
                 (PyObject *)NULL, (*pp_stack)-n, na,    // 位置参数的信息
                 (*pp_stack)-2*nk, nk,        // 键参数信息
                                 d, nd,    // 函数默认参数信息
                 PyFunction_GET_CLOSURE(func));
}
f()调用时的动态信息

由于有默认值的PyFunctionObject对象argdefs != NULL,所以代码会进入PyEval_EvalCodeEx()函数,将默认参数取出作为参数传递给PyEval_EvalCodeEx()函数。

PyObject *
PyEval_EvalCodeEx(PyCodeObject *co, PyObject *globals, PyObject *locals,
       PyObject **args, int argcount, PyObject **kws, int kwcount,
       PyObject **defs, int defcount, PyObject *closure)
{
    register PyFrameObject *f;
    register PyObject *retval = NULL;
    register PyObject **fastlocals, **freevars;
    PyThreadState *tstate = PyThreadState_GET();
    PyObject *x, *u;
    // 创建新的PyFrameObject对象
    f = PyFrame_New(tstate, co, globals, locals);

    fastlocals = f->f_localsplus;
    freevars = f->f_localsplus + co->co_nlocals;

    if (co->co_argcount > 0 || co->co_flags & (CO_VARARGS | CO_VARKEYWORDS)) {
        int i;
        // n为CALL_FUNCTION的参数指示的传入的位置参数的个数,即na
        int n = argcount;
        ......
        // 判断是否使用参数的默认值
        if (argcount < co->co_argcount) {
            // m = 位置参数总数 - 被设置了默认值的位置参数个数
            int m = co->co_argcount - defcount;
            // 函数调用者必须传递一般位置参数的参数值
            for (i = argcount; i < m; i++) {
                if (GETLOCAL(i) == NULL) {
                    goto fail;
                }
            }
            // n>m意味着调用者希望替换一些默认位置参数的默认值
            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);
                }
            }
        }
    }
    
    retval = PyEval_EvalFrameEx(f,0);
    return retval;
}
  • 默认位置参数是指定了默认值的位置参数;一般位置参数是没有指定默认值的位置参数
  • argcount < co->co_argcount用来判断是否需要使用参数的默认值,当调用函数传递的位置参数的个数小于函数编译后的PyCodeObject对象中co_argcount指定的参数个数时,则表示Python虚拟机需要为函数设置默认参数。
  • m = co->co_argcount - defcount中,m表示一般位置参数的个数
  • i = n - m来确定从哪个默认位置参数开始设定参数的默认值(def g(a, b, c=1, d=2)g(3, 3, 3)),这个例子中,n=3(函数调用时传递的位置参数的个数),m=2(一般位置参数的个数),所以n-m=1(表示从第一个位置参数的地方开始设置默认值,即为d=2设置默认值)
  • 然后从PyFrameObject对象的func_defaults中将这些参数取出,通过SETLOCAL将其放入PyFrameObject对象的f_localsplus所管理的内存块中。i表示需要在f_localsplus中设置默认值的位置,它从第一个需要设置默认值的默认参数位置开始,依次向后。
f(age=10)方式调用
f(age=10)调用时的动态信息

在进入fast_function()时,仍然不会进入快速通道。最后还是进入PyEval_EvalCodeEx()函数中,其中argcount就是nakwcount就是nk

PyEval_EvalCodeEx中各参数含义

f()的调用相比,我们关心的是age=10是如何替换默认参数的。

  • 核心算法就是:在编译时,Python编译器会将函数的def语句中出现的参数名称都记录在变量co_varname中。调用f(age=10)时(CALL_FUNCTION字节码指令),会将age10都压入运行时栈中,在PyEval_EvalCodeEx()函数中,就从运行时栈中拿出键参数的名称,在co_varname中查找,查找到以后,根据查找到的参数索引,直接设置f_localsplus中的内存值为10,这为默认参数位置设置了函数调用者希望的值。
  • 上面查找到的索引为什么可以直接用来设置f_localsplus的内存呢?
    • 因为编译得到的co_varnamef_localsplus中存放参数的顺序都是按照def语句定义的顺序进行排放的,所以可以直接使用位置参数的索引。

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

扩展位置参数(*list)和扩展建参数(**kwargs),实际上是作为函数的局部变量来实现的。

Python在编译一个函数时,如果发现其形式参数中存在*list这样的扩展位置参数,那么Python会在编译所得的PyCodeObject对象的co_flags中添加一个标识符CO_VARARGS,表示该函数在被调用时需要处理扩展位置参数;同理扩展键参数(**kwargs)会添加CO_VARKEYWORDS标识。

处理完正规位置参数以后,就开始处理扩展位置参数,将所有扩展位置参数加入到PyTupleObject对象中,然后放入到f_localsplus内存中,它的内存位置就在正规位置参数的下一个位置。

对于扩展键参数,会创建一个PyDictObject对象,对所有的键参数进行过滤,将扩展键参数插入到PyDictObject对象中,扩展键参数PyDictObject被插入到f_localsplus内存中扩展位置参数PyTupleObject的下一个位置。

f_localsplus中各参数展示

0x05 函数中局部变量的访问

按照之前的思维,当需要访问局部变量时,应该到local名字空间中去搜索变量名。但是,在调用函数期间,局部变量和函数参数一样,存储在f_localsplus中。

为什么函数中没有local名字空间?

  • 因为函数中的局部变量总是固定不变的。所有在编译的时候就能确定局部变量使用的内存空间的位置,也能确定访问局部变量的字节码指令应该如何访问内存。
  • 函数中,Python可以使用静态的方法来实现局部变量,而不需要借助于动态的查找PyDictObject对象的技术,毕竟静态方法可以极大的提高函数执行的效率。

0x06 嵌套函数、闭包与decorator

名字空间是在运行时由Python虚拟机动态维护的,但是有时候我们需要将名字空间静态化。换句话说就是,我们希望有的代码不受名字空间变换的影响,始终保持一致的行为和结果。

使用闭包(closure)来实现这种场景:内嵌函数被返回的时候,会将使用到的名字空间和函数捆绑在一起,然后返回,这就形成了一个闭包。

闭包是最内嵌套规则实现方式,不使用闭包也能实现最内嵌套规则。

不使用闭包实现最内嵌套规则

实现闭包的基石

闭包的创建通常是利用嵌套函数来完成的。在PyCodeObject中,与嵌套函数相关的属性是co_cellvarsco_freevars

  • co_cellvars:通常是一个tuple,保存嵌套的作用域中使用的变量名集合
  • co_freevars:通常也是一个tuple,保存使用了的外层作用域中的变量名集合
# example01.py
def get_func():
    value = "inner"
    def inner_func():
        print value
    return inner_func

show_value = get_func()
show_value()

上例中会产生3PyCodeObject对象,与get_func对应的PyCodeObject对象中的co_cellvars就应该包含字符串value,因为其嵌套作用域(inner_func的作用域)中使用了这个符号。

同理,与函数inner_func对应的PyCodeObject对象中的co_freevars中应该也有字符串value

PyFrameObject对象中,也有一个属性与闭包的实现相关,就是f_localsplusPyFrame_New函数中的代码extras = code->co_stacksize + code->co_nlocals + ncells + nfrees,表示了f_localsplus属于4个元素:运行时栈、局部变量、cellco_cellvars)对象和freeco_freevars)对象。

PyFrameObject对象中f_localsplus完整内存布局

闭包的实现

# example01.py的字节码指令
# def get_func():
1           0 LOAD_CONST               0 (<code object get_func at 00000000041EEE30, file "test3.py", line 1>)
            3 MAKE_FUNCTION            0
            6 STORE_NAME               0 (get_func)
    # value = "inner"
    2           0 LOAD_CONST               1 ('inner')
                3 STORE_DEREF              0 (value)
    # def inner_func():
    3           6 LOAD_CLOSURE             0 (value)
                9 BUILD_TUPLE              1
               12 LOAD_CONST               2 (<code object inner_func at 00000000041EECB0, file "test3.py", line 3>)
               15 MAKE_CLOSURE             0
               18 STORE_FAST               0 (inner_func)
        # print value
        4           0 LOAD_DEREF               0 (value)
                    3 PRINT_ITEM
                    4 PRINT_NEWLINE
                    5 LOAD_CONST               0 (None)
                    8 RETURN_VALUE
    # return inner_func
    5          21 LOAD_FAST                0 (inner_func)
               24 RETURN_VALUE

# show_value = get_func()
7           9 LOAD_NAME                0 (get_func)
           12 CALL_FUNCTION            0
           15 STORE_NAME               1 (show_value)
# show_value()
8          18 LOAD_NAME                1 (show_value)
           21 CALL_FUNCTION            0
           24 POP_TOP
           25 LOAD_CONST               1 (None)
           28 RETURN_VALUE
创建closure

调用12 CALL_FUNCTION 0时,会进入fast_function函数,由于co_flag3CO_OPTIMIZED|CO_NEWLOCALS),所以不会进入快速通道,最终调用到PyEval_EvalCodeEx

PyEval_EvalCodeEx中,Python虚拟机会如同处理默认参数一样,将co_cellvars中的东西拷贝到新创建的PyFrameObjectf_localsplus中。

PyObject *
PyEval_EvalCodeEx(PyCodeObject *co, PyObject *globals, PyObject *locals,
       PyObject **args, int argcount, PyObject **kws, int kwcount,
       PyObject **defs, int defcount, PyObject *closure)
{
    for (i = 0; i < PyTuple_GET_SIZE(co->co_cellvars); ++i) {
        // 获得被嵌套函数共享的符号名
        cellname = PyString_AS_STRING(
            PyTuple_GET_ITEM(co->co_cellvars, i));
        found = 0;
        for (j = 0; j < nargs; j++) {
            argname = PyString_AS_STRING(
                PyTuple_GET_ITEM(co->co_varnames, j));
            if (strcmp(cellname, argname) == 0) {
                c = PyCell_New(GETLOCAL(j));
                if (c == NULL)
                    goto fail;
                GETLOCAL(co->co_nlocals + i) = c;
                found = 1;
                break;
            }
        }
        // 处理被嵌套函数共享外层函数的默认参数
        if (found == 0) {
            c = PyCell_New(NULL);
            if (c == NULL)
                goto fail;
            SETLOCAL(co->co_nlocals + i, c);
        }
    }
}

其中found变量表示:被内层嵌套函数引用的符号是否已经与某个值绑定的标识,或者说与某个对象已经建立约束的标识。只有在内层嵌套函数引用的是外层函数的一个有默认值的参数时,这个标识才可能为1

Python虚拟机接下来会创建一个PyCellObject对象,这个对象很简单,只维护了一个指针。

随后在value = "inner"中被赋值,接下来将cell对象拷贝到新创建的PyFrameObject对象的f_localsplus中,拷贝的位置在co->co_nlocals+icell对象的位置在局部变量之后)。

开始执行get_func函数,3 STORE_DEREF 0 (value)做的动作是,从运行时栈拿出inner对象,从f_localsplus中取出PyCellObject对象,即对PyCellObject对象赋值。

设置cell对象之后的f_localsplus

接下来就是形成闭包的关键,在执行def inner_func():时,Python虚拟机会将(value,"inner")这个约束塞到PyFunctionObject对象中。

TARGET(MAKE_CLOSURE)
{
    // 获得PyCodeObject对象
    v = POP(); /* code object */
    // 绑定global名字空间
    x = PyFunction_New(v, f->f_globals);
    Py_DECREF(v);
    if (x != NULL) {
        // 获得tuple对象,其中包含PyCellObject对象
        v = POP();
        // 绑定约束集合
        if (PyFunction_SetClosure(x, v) != 0) {
            /* Can't happen unless bytecode is corrupt. */
            why = WHY_EXCEPTION;
        }
        Py_DECREF(v);
    }
    PUSH(x);
    break;
}

6 LOAD_CLOSURE 0 (value)PyCellObject对象取出,放入运行时栈中,接下来9 BUILD_TUPLE 1指令将PyCellObject对象打包进一个tuple对象中,最后通过15 MAKE_CLOSURE 0指令完成约束与PyCodeObject对象的绑定。

18 STORE_FAST 0 (inner_func)将新创建的PyFunctionObject对象放置到f_localsplus中,最后再将新建的这个PyFunctionObject对象压入运行时栈,然后返回。

closure创建完成以后
使用closure

closure是在get_func中被创建,而对closure的使用,则是在inner_func中。

在执行show_value()CALL_FUNCTION指令时,和inner_func对应的PyCodeObject对象中的co_flags包含CO_NESTED,所以最后还是进入PyEval_EvalCodeEx中。

inner_func编译后的PyCodeObject对象中co_freevars里面有引用的外层作用域中的符号名,所以需要进行处理,如下所示:

PyObject *
PyEval_EvalCodeEx(PyCodeObject *co, PyObject *globals, PyObject *locals,
           PyObject **args, int argcount, PyObject **kws, int kwcount,
           PyObject **defs, int defcount, PyObject *closure)
{
......
    if (PyTuple_GET_SIZE(co->co_freevars)) {
        int i;
        for (i = 0; i < PyTuple_GET_SIZE(co->co_freevars); ++i) {
            PyObject *o = PyTuple_GET_ITEM(closure, i);
            Py_INCREF(o);
            freevars[PyTuple_GET_SIZE(co->co_cellvars) + i] = o;
        }
    }
......
}

上面代码中closurefast_function传进来的,其实就是在PyFunctionObject对象中与PyCodeObject对象绑定的装满了PyCellObject对象的tuple,所以在PyEval_EvalCodeEx中,进行的动作就是将这个PyCellObject对象一个一个放入到f_localsplus中相应的位置。处理完closure后,inner_func对应的PyFrameObject中的f_localsplus如下所示:

将closure设置到当前PyFrameObject对象中

inner_func调用的过程中,当引用外层作用域的符号时,一定是到f_localsplus中的free变量区域中获取对应的值。这正好对应的是print value对应的字节码指令0 LOAD_DEREF 0 (value)

Decorator

decorator只是closure的一种包装形式,只是对外展示的语法格式不同而已。


欢迎关注微信公众号(coder0x00)或扫描下方二维码关注,我们将持续搜寻程序员必备基础技能包提供给大家。


相关文章

网友评论

      本文标题:【Python】虚拟机中的函数机制

      本文链接:https://www.haomeiwen.com/subject/upkrhqtx.html