Python内核阅读(十): 虚拟机中的一般表达式

Python 2017-08-08

起步

上一章介绍了Python虚拟机的整理框架, 接下来将深入到 PyEval_EvalFrameEx 的细节中. 将分析python虚拟机是如果完成一般表达式的. 一般表达式包括基本的对象创建, 打印语句等. 像 if, while这类的属于控制语句.

内建对象的创建

对于一个简单的对象:

[simple_obj.py]
i = 1
s = 'python'
l = []
d = {}

利用前面提到的, 我们可以访问其code对象:

co_argcount : 0
co_cellvars : ()
co_consts : (1, 'python', None)
co_filename : simple_obj.py
co_firstlineno : 1
co_flags : 64
co_freevars : ()
co_kwonlyargcount : 0
co_name : <module>
co_names : ('i', 's', 'l', 'd')
co_nlocals : 0
co_stacksize : 1
co_varnames : ()

以及这段代码所需要执行的字节码命令:

  1           0 LOAD_CONST               0 (1)
              2 STORE_NAME               0 (i)

  2           4 LOAD_CONST               1 ('python')
              6 STORE_NAME               1 (s)

  3           8 BUILD_LIST               0
             10 STORE_NAME               2 (l)

  4          12 BUILD_MAP                0
             14 STORE_NAME               3 (d)
             16 LOAD_CONST               2 (None)
             18 RETURN_VALUE

在执行字节码指令时,会用到大量的宏, 本章可能用到的宏有:

[ceval.c]
// 获取tuple中的元素
#define GETITEM(v, i) PyTuple_GET_ITEM((PyTupleObject *)(v), (i))

// 调整栈顶指针
#define BASIC_STACKADJ(n) (stack_pointer += n)

// 入栈操作
#define BASIC_PUSH(v)     (*stack_pointer++ = (v))
#define PUSH(v)                BASIC_PUSH(v)

// 出栈操作
#define BASIC_POP()       (*--stack_pointer)
#define POP()                  BASIC_POP()

先来看看simple_obj.py中第一行对应的字节码指令:

1           0 LOAD_CONST               0 (1)
            2 STORE_NAME               0 (i)

PyFrameObject对象运行时, 字节码指令对符号或常量的操作最终都反应到运行时栈或local名字空间中f->f_locals.

LOAD_CONST 指令的实现代码:

TARGET(LOAD_CONST) {
    PyObject *value = GETITEM(consts, oparg);
    Py_INCREF(value);
    PUSH(value);
    FAST_DISPATCH();
}

GETITEM(consts, oparg) 明显就是 GETITEM(consts, 0) ,即 PyTuple_GET_ITEM(consts, 0), 从consts中读取序号为0的元素, 然后将其压入运行时栈. 联系上下文可以得到consts实际上就是 f->f_code->co_consts ,f是当前活动的PyFrameObject对象. co_consts经常被称作是常量表.

第一条字节码指令只改变了运行时栈, 未对local名字空间产生影响, 而执行 i = 1 这个赋值表达式实际应该在local名字空间创建一个符号i到PyIntObject对象1的映射关系. python虚拟机通过 STORE_NAME 来改变local名字空间:

TARGET(STORE_NAME) {
    // 从符号表中获取符号 这里 oparg = 0
    PyObject *name = GETITEM(names, oparg);
    // 从运行时栈中获得值
    PyObject *v = POP();
    PyObject *ns = f->f_locals;
    int err;

    if (PyDict_CheckExact(ns))
        err = PyDict_SetItem(ns, name, v);
    else
        err = PyObject_SetItem(ns, name, v);
    Py_DECREF(v);
    if (err != 0)
        goto error;
    DISPATCH();
}

names是 f->f_code->co_names 也就是符号表 , ns = f->f_locals 可知操作的对象是local名字空间, 它是一个PyDictObject对象. 将(变量名, 变量值)元素加到字典里去.

再看看第二行python代码s = 'python' 的指令:

  2           4 LOAD_CONST               1 ('python')
              6 STORE_NAME               1 (s)

和上一行的字节码差不多, 只是参数获取的索引变了.

第三行 l = [] 的指令:

  3           8 BUILD_LIST               0
             10 STORE_NAME               2 (l)

BUILD_LIST实现方式:

TARGET(BUILD_LIST) {
    PyObject *list =  PyList_New(oparg);
    while (--oparg >= 0) {
        PyObject *item = POP();
        PyList_SET_ITEM(list, oparg, item);
    }
    PUSH(list);
    DISPATCH();
}

显然,如果要创建不是一个空的list, 就会从栈中一一弹出元素, 加入到刚创建的list中.

第四行代码 d = {} 对应的指令:

  4          12 BUILD_MAP                0
             14 STORE_NAME               3 (d)
             16 LOAD_CONST               2 (None)
             18 RETURN_VALUE

BUILD_MAP看样子是创建一格dict对象:

TARGET(BUILD_MAP) {
    Py_ssize_t i;
    PyObject *map = _PyDict_NewPresized((Py_ssize_t)oparg);

    for (i = oparg; i > 0; i--) {
        int err;
        // 从栈中获取key和value
        PyObject *key = PEEK(2*i);
        PyObject *value = PEEK(2*i - 1);
        err = PyDict_SetItem(map, key, value);
        if (err != 0) {
            Py_DECREF(map);
            goto error;
        }
    }
    // 参数出栈
    while (oparg--) {
        Py_DECREF(POP());
        Py_DECREF(POP());
    }
    PUSH(map);
    DISPATCH();
}

当创建不是一个空字典时, 会一一从栈中获取元素. 在python中, 执行一段Code Block后一定要返回一些值:

TARGET(RETURN_VALUE) {
    retval = POP();
    why = WHY_RETURN;
    goto fast_block_end;
}

实际的返回值保存在 retval 中, 是从运行时栈中取得的, 因此它的值就是上一条的 LOAD_CONST 2 (None) 了, 即None值. 然后将虚拟机状态设为 WHY_RETURN .

复杂的内建对象创建

前面说的当创建不是一个空list或空dict时, 是会从栈中获取元素, 那他们是怎么进入栈的呢, 看看测试代码:

l = [1, 2]
d = {"1": 1, "2": 2}

对应的code和字节码指令:

co_argcount : 0
co_cellvars : ()
co_consts : (1, 2, ('1', '2'), None)
co_filename : simple_obj.py
co_firstlineno : 1
co_flags : 64
co_freevars : ()
co_kwonlyargcount : 0
co_name : <module>
co_names : ('l', 'd')
co_nlocals : 0
co_stacksize : 3
co_varnames : ()
1         0 LOAD_CONST               0 (1)
          2 LOAD_CONST               1 (2)
          4 BUILD_LIST               2
          6 STORE_NAME               0 (l)

2         8 LOAD_CONST               0 (1)
         10 LOAD_CONST               1 (2)
         12 LOAD_CONST               2 (('1', '2'))
         14 BUILD_CONST_KEY_MAP      2
         16 STORE_NAME               1 (d)
         18 LOAD_CONST               3 (None)
         20 RETURN_VALUE

code中比较重要的是常量表co_consts : (1, 2, ('1', '2'), None) 和符号表 co_names : ('l', 'd'). list的创建之前会先load常量到栈中, BUILD_LIST 传入参数2, 表示需要两个参数, 正好从栈中获取2个.

创建dict前, 会先load他们values, 然后将keys入栈,再创建dict:

TARGET(BUILD_CONST_KEY_MAP) {
    Py_ssize_t i;
    PyObject *map;
    PyObject *keys = TOP();

    map = _PyDict_NewPresized((Py_ssize_t)oparg);

    for (i = oparg; i > 0; i--) {
        int err;
        PyObject *key = PyTuple_GET_ITEM(keys, oparg - i);
        PyObject *value = PEEK(i + 1);
        err = PyDict_SetItem(map, key, value);
        if (err != 0) {
            Py_DECREF(map);
            goto error;
        }
    }

    Py_DECREF(POP());
    while (oparg--) {
        Py_DECREF(POP());
    }
    PUSH(map);
    DISPATCH();
}

BUILD_CONST_KEY_MAPBUILD_MAP 的实现差不多, 有个搞不懂的, 既然创建空dict和非空dict的指令不一样, 那为什么创建空dict需要参数呢? 这个没理解.

其他的一般表达式

如果对于需要运算的操作实现过程是怎样呢:

a = 5
b = a
c = a + b
print(c)

对应的code和字节码指令:

co_consts : (5, None)
co_filename : simple_obj.py
co_names : ('a', 'b', 'c', 'print')
co_nlocals : 0
co_stacksize : 2

1         0 LOAD_CONST               0 (5)
          2 STORE_NAME               0 (a)

2         4 LOAD_NAME                0 (a)
          6 STORE_NAME               1 (b)

3         8 LOAD_NAME                0 (a)
         10 LOAD_NAME                1 (b)
         12 BINARY_ADD
         14 STORE_NAME               2 (c)

4        16 LOAD_NAME                3 (print)
         18 LOAD_NAME                2 (c)
         20 CALL_FUNCTION            1
         22 POP_TOP
         24 LOAD_CONST               1 (None)
         26 RETURN_VALUE

符号搜索

有了一个新的指令 LOAD_NAME , 从符号表中载入, 根据后面的STORE_NAME, 应该是将符号表对应的变量值取出放到栈里面:

// 有删改
TARGET(LOAD_NAME) {
    PyObject *name = GETITEM(names, oparg);
    PyObject *locals = f->f_locals;
    PyObject *v;

    v = PyDict_GetItem(locals, name);
    Py_XINCREF(v);

    if (v == NULL) {
        v = PyDict_GetItem(f->f_globals, name);
        Py_XINCREF(v);
        if (v == NULL) {
            v = PyDict_GetItem(f->f_builtins, name);
            if (v == NULL) {
                format_exc_check_arg(
                            PyExc_NameError,
                            NAME_ERROR_MSG, name);
                goto error;
            }
            Py_INCREF(v);
        }
    }
    PUSH(v);
    DISPATCH();
}

LOAD_NAME 将以此从local, global, builtin 3个名字空间顺序查找, 如果都没找到说明名字未定义, 抛出异常, 终止python虚拟机的运行. 搜索规则也就是 LGB 规则.

数值运算

数值运算 c = a + b 中看到了新的指令 BINARY_ADD , 虚拟机先通过load_name将a和b的变量值取出压入运行时栈, 然后再进行的加法:

TARGET(BINARY_ADD) {
    PyObject *right = POP();
    PyObject *left = TOP();
    PyObject *sum;

    if (PyUnicode_CheckExact(left) &&
             PyUnicode_CheckExact(right)) {
        // 字符串拼接
        sum = unicode_concatenate(left, right, f, next_instr);
    }
    else {
        // 数值相加
        sum = PyNumber_Add(left, right);
        Py_DECREF(left);
    }
    Py_DECREF(right);
    SET_TOP(sum);
    if (sum == NULL)
        goto error;
    DISPATCH();
}

加法运算除了用于数字也同样用于字符串, 在 PyNumber_Add 中, 虚拟机进行大量的类型判断, 检查PyNumberMethods中的nb_add能否完成加法运算. 如果不能, 会检查PySequenceMethods中sq_concat能否完成. 如果不能,python就报错误了.

信息输出

最后看一看print的动作, python3中print是一个函数, 因此需要将函数和参数事先压入栈中:

16 LOAD_NAME                3 (print)
18 LOAD_NAME                2 (c)
20 CALL_FUNCTION            1
22 POP_TOP

CALL_FUNCTION 进行函数调用, 参数数量是1:

TARGET(CALL_FUNCTION) {
    PyObject **sp, *res;
    sp = stack_pointer;
    // oparg为1
    res = call_function(&sp, oparg, NULL);
    stack_pointer = sp; // 重设栈指针, 模拟了CPU设计
    PUSH(res);
    if (res == NULL) {
        goto error;
    }
    DISPATCH();
}

这里显然 print 是python的内建函数, 关于函数机制后面看, 这里先跳过它的获取过程, 看print的实现部分:

[bltinmoudule.c]
static PyMethodDef builtin_methods[] = {
    ...
    {"print",           (PyCFunction)builtin_print,      METH_FASTCALL | METH_KEYWORDS, print_doc}
    ...
}

实现在 builtin_print 中:

static PyObject * builtin_print(PyObject *self, PyObject **args, Py_ssize_t nargs, PyObject *kwnames)
{
    ...
    file = _PySys_GetObjectId(&PyId_stdout);
    for (i = 0; i < nargs; i++) {
        if (i > 0) {
            if (sep == NULL)
                err = PyFile_WriteString(" ", file);
            else
                err = PyFile_WriteObject(sep, file,
                                         Py_PRINT_RAW);
            if (err)
                return NULL;
        }
        err = PyFile_WriteObject(args[i], file, Py_PRINT_RAW);
        if (err)
            return NULL;
    }

    if (end == NULL)
        err = PyFile_WriteString("\n", file);
    else
        err = PyFile_WriteObject(end, file, Py_PRINT_RAW);
    if (err)
        return NULL;
    ...

    Py_RETURN_NONE;
}

默认情况输出到 stdout 标准输出中, 使用PyFile_WriteObject将元素输出, 当有多个需要打印时, 用空格 " " 隔开, 并且最后输出一个换行 \n .


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

如果对您有用,您的支持将鼓励我继续创作!