blog icon indicating copy to clipboard operation
blog copied to clipboard

Python的简单赋值语句

Open junnplus opened this issue 7 years ago • 2 comments

这篇文章主要是来剖析以下的代码片段的执行:

a = 0
b = ''
c = []
d = {}

利用compile获得PyCodeObject对象:

>>> co = compile(open('demo.py').read(), '', 'exec')
>>> co.co_names
('a', 'b', 'c', 'd')
>>> co.co_consts
(0, '', None)

co.co_namesco.co_consts分别是Code Block中的符号集合和常量集合,都是PyTupleObject对象。

我们主要是通过字节码来对简单赋值语句的剖析:

>>> import dis
>>> dis.dis(co)
  1           0 LOAD_CONST               0 (0)
              2 STORE_NAME               0 (a)

  2           4 LOAD_CONST               1 ('')
              6 STORE_NAME               1 (b)

  3           8 BUILD_LIST               0
             10 STORE_NAME               2 (c)

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

剖析之前我们先看下本文需要用到的一些宏定义:

#define GETITEM(v, i) PyTuple_GetItem((v), (i))
#define PEEK(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()
  • GETITEM是通过下标来访问tuple中的元素。
  • PEEK, PUSH, POP都是对“运行时栈”的操作。stack_pointer当前指向“运行时栈”的栈底位置。

a = 0赋值语句对应了两条字节码指令:

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

在对Python虚拟机的剖析中,我们的重点将放在字节码指令将如何影响当前活动的PyFrameObject对象中的“运行时栈”和local名字空间(f->f_locals)。

字节码指令对应的实现代码都位于Python/ceval.c中,我们先来看LOAD_CONST指令:

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

consts对应co.co_consts常量集合,oparg是指令操作参数,对应第四列的值。

LOAD_CONST指令的操作很明显,从consts元组中读取下标0的PyObject对象0,然后push压入“运行时栈”。 这个指令只改变了“运行时栈”,对local名字空间没有任何影响。

TARGET(STORE_NAME) {
    PyObject *name = GETITEM(names, oparg);
    PyObject *v = POP();
    PyObject *ns = f->f_locals;
    int err;
    if (ns == NULL) {
        PyErr_Format(PyExc_SystemError,
                     "no locals found when storing %R", name);
        Py_DECREF(v);
        goto error;
    }
    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();
}

STORE_NAME指令从names符号集合(co.co_names)中读取对应的变量名a,然后pop出栈操作获取变量值0, 将a -> 0这样的映射关系添加到local名字空间(f->f_locals)中。这边我们只考虑f->f_locals确实是PyDictObject对象的情况。

STORE_NAME指令同时改变了“运行时栈”和local名字空间的状态。

b = ''语句和上一句类似,最终在local名字空间里面建立了b -> ''这样的映射关系。

c = []语句的字节码指令不再是LOAD_CONST了:

  3           8 BUILD_LIST               0
             10 STORE_NAME               2 (c)
TARGET(BUILD_LIST) {
    PyObject *list =  PyList_New(oparg);
    if (list == NULL)
        goto error;
    while (--oparg >= 0) {
        PyObject *item = POP();
        PyList_SET_ITEM(list, oparg, item);
    }
    PUSH(list);
    DISPATCH();
}

对于BUILD_LIST指令,将会创建一个oparg大小的list,然后依次出栈加入到新创建的PyListObject对象中。所以可以推测,如果oparg参数不为0的话,在BUILD_LIST指令之前一定会有许多LOAD_CONST的操作,LOAD_CONST指令压栈的对象就是list中的元素。 新创建的PyListObject对象最后还会被压入栈中。

d = {}语句的字节码指令就和前一句类似:

  4          12 BUILD_MAP                0
             14 STORE_NAME               3 (d)

BUILD_MAP看上去比BUILD_LIST复杂多了。

TARGET(BUILD_MAP) {
    Py_ssize_t i;
    PyObject *map = _PyDict_NewPresized((Py_ssize_t)oparg);
    if (map == NULL)
        goto error;
    for (i = oparg; i > 0; i--) {
        int err;
        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();
}

这边的oparg参数表示了有多少对(key, value)元素。

BUILD_MAP指令通过_PyDict_NewPresized创建一个PyDictObject对象:

PyObject *
_PyDict_NewPresized(Py_ssize_t minused)
{
    const Py_ssize_t max_presize = 128 * 1024;
    Py_ssize_t newsize;
    PyDictKeysObject *new_keys;

    /* There are no strict guarantee that returned dict can contain minused
     * items without resize.  So we create medium size dict instead of very
     * large dict or MemoryError.
     */
    if (minused > USABLE_FRACTION(max_presize)) {
        newsize = max_presize;
    }
    else {
        Py_ssize_t minsize = ESTIMATE_SIZE(minused);
        newsize = PyDict_MINSIZE;
        while (newsize < minsize) {
            newsize <<= 1;
        }
    }
    assert(IS_POWER_OF_2(newsize));

    new_keys = new_keys_object(newsize);
    if (new_keys == NULL)
        return NULL;
    return new_dict(new_keys, NULL);
}

_PyDict_NewPresized主要是计算新创建的PyDictObject对象需要的内存大小。

回到BUILD_MAP指令,key和value分别通过PEEK(2*i)PEEK(2*i - 1)宏来读取,可见BUILD_MAP之前的LOAD_CONST指令应该也是成对出现的。最后成对出栈,PyDictObject对象再压入栈中。

最后两条指令是Python在执行了一段Code Block结束后,会有返回值:

             16 LOAD_CONST               2 (None)
             18 RETURN_VALUE

LOAD_CONST指令将NoneObject对象压栈,以供RETURN_VALUE指令使用。

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

实际的返回值存在retval中,是从“运行时栈”中取得的。

junnplus avatar Apr 02 '18 17:04 junnplus

最近是在看CPython的源码吗?楼主

jiajunhuang avatar Apr 03 '18 01:04 jiajunhuang

@jiajunhuang 嗯,看了有一段时间了

junnplus avatar Apr 03 '18 02:04 junnplus