blog
blog copied to clipboard
Python的简单赋值语句
这篇文章主要是来剖析以下的代码片段的执行:
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_names和co.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中,是从“运行时栈”中取得的。
最近是在看CPython的源码吗?楼主
@jiajunhuang 嗯,看了有一段时间了