还是从字节码开始分析
a = 1
b = 2
c = a + b
编译:
0 LOAD_CONST 0 (1)
2 STORE_NAME 0 (a)
4 LOAD_CONST 1 (2)
6 STORE_NAME 1 (b)
8 LOAD_NAME 0 (a)
10 LOAD_NAME 1 (b)
12 BINARY_ADD
14 STORE_NAME 2 (c)
前面两个变量绑定上节已经分析过了,我们从第八个字节开始分析,by the way,python3.6开始字节码变成了固定两个字节。
首先是LOAD_NAME这个字节码,顾名思义,应该是将名字对应的值加载到栈顶,让我们看看是不是这样:
TARGET(LOAD_NAME) {
PyObject *name = GETITEM(names, oparg);
PyObject *locals = f->f_locals;
PyObject *v;
if (locals == NULL) {
PyErr_Format(PyExc_SystemError,
"no locals when loading %R", name);
goto error;
}
if (PyDict_CheckExact(locals)) {
v = PyDict_GetItem(locals, name);
Py_XINCREF(v);
}
else {
v = PyObject_GetItem(locals, name);
if (v == NULL) {
if (!PyErr_ExceptionMatches(PyExc_KeyError))
goto error;
PyErr_Clear();
}
}
if (v == NULL) {
v = PyDict_GetItem(f->f_globals, name);
Py_XINCREF(v);
if (v == NULL) {
if (PyDict_CheckExact(f->f_builtins)) {
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);
}
else {
v = PyObject_GetItem(f->f_builtins, name);
if (v == NULL) {
if (PyErr_ExceptionMatches(PyExc_KeyError))
format_exc_check_arg(
PyExc_NameError,
NAME_ERROR_MSG, name);
goto error;
}
}
}
}
PUSH(v);
DISPATCH();
}
足足50行,加载个名字而已,为啥这么麻烦。
第一行GETITEM ,看看宏定义:
#define GETITEM(v, i) PyTuple_GetItem((v), (i))
原来names域是一个tuple,好吧,第一步貌似就已经将名字取到了,接下来还要做啥?当然是取名字对应的值了。
后面的事情简直不能更简单,注意这3行:
v = PyDict_GetItem(locals, name);
v = PyDict_GetItem(f->f_globals, name);
v = PyDict_GetItem(f->f_builtins, name);
这就是python寻找变量的值的顺序了,依次分别尝试从locals,globals,builtins里面去寻找这个名字对应的值,最后:
PUSH(v);
将找到的值压入栈顶,完了。
至于那些个
Py_INCREF(v);
这些东西,是增加垃圾回收引用计数,对我们的分析没有影响,就不管他了。
还有一个问题:最后一行那个DISPATHC()又是啥?
点进去一看,哈哈,就是个continue。
下一步
接着两个LOAD_NAME的是一个BINARY_ADD,想都不用想,一定是把两个栈顶的值POP出来加起来,然后将结果放回栈顶,让我们验证一下:
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);
/* unicode_concatenate consumed the ref to left */
}
else {
sum = PyNumber_Add(left, right);
Py_DECREF(left);
}
Py_DECREF(right);
SET_TOP(sum);
if (sum == NULL)
goto error;
DISPATCH();
}
可以说代码是非常的短了,看看做了些什么:
PyObject *right = POP();
PyObject *left = TOP();
PyObject *sum;
首先是取出两个加数,然后定义了和的指针,注意left是TOP取出来的,说明此时left还在栈顶,然后发生了一些奇怪的事情:
if (PyUnicode_CheckExact(left) &&
PyUnicode_CheckExact(right)) {
sum = unicode_concatenate(left, right, f, next_instr);
}
居然可以做字符串加法。。还好我们传入的都是数字,所以应该到了else里面:
else {
sum = PyNumber_Add(left, right);
Py_DECREF(left);
}
Py_DECREF(right);
SET_TOP(sum);
结果为sum,SET_TOP这个宏直接将栈顶元素指向了sum,此时栈顶就是计算结果了。
还差最后一步,将结果赋值给c,STORE_NAME,我们已经不能更熟悉。。略。
总结一下
从最初的轻视,到发现一个LOAD_NAME就有50行代码的惊讶,再到最后的释然,这就是源码分析时的快感,让源码调动起自己的情绪,随着代码去旅行,可能就是最好的分析方法。