Python源码剖析
——Python执行引擎之一般表达式(2)
本文作者: Robert Chen([email protected] )
前面我们看了创建空的dict对象和空的list,那么如果是创建非空的dict和list时,行为又是如何的呢。这个问题很有趣,我们通过simple.py来研究:
[simple.py]
i = 1
s = "Python"
d = {"1":1, "2":2}
l = [1, 2]
对于simple.py,执行时的常量表co_consts自然和declare.py不同了,图11显示了与simple.py对应的co_consts和co_names:
编译得到的字节码中,前两行Python代码的字节码都是相同的,在创建非空的dict时,字节码与declare.py中的不同了:
d = {"1":1, "2":2}
# BUILD_MAP 0
# DUP_TOP
# LOAD_CONST 2 (‘1’)
# LOAD_CONST 0 (1)
# ROT_THREE
# STORE_SUBSCR
# DUP_TOP
# LOAD_CONST 3 (‘2’)
# LOAD_CONST 4 (2)
# ROT_THREE
# STORE_SUBSCR
# STORE_NAME 2 (d)
对于DUP_TOP,Python会进行如下的动作:
[DUP_TOP]
v = TOP();
Py_INCREF(v);
PUSH(v);
不要注意的是,DUP_TOP不光增加了栈顶元素的引用计数,还将栈顶元素又一次第压入到栈中。由于在DUP_TOP之前是一个BUILD_MAP,所以会将创建的PyDictObject对象的引用计数增加1,并再次压入该PyDictObject对象。
然后会从consts中将需要插入到PyDictObject对象中的第一个元素对的键和值读取出来,都压入到栈中,完成后的情形如图12所示:
在接下来的ROT_THREE中,会做一些奇怪的动作:
[ROT_THREE]
v = TOP();
w = SECOND();
x = THIRD();
SET_TOP(w);
SET_SECOND(x);
SET_THIRD(v);
其中的SET_TOP等宏也是在PyEval_EvalFrame中定义的:
[ceval.c]
#define TOP() (stack_pointer[-1])
#define SECOND() (stack_pointer[-2])
#define THIRD() (stack_pointer[-3])
#define FOURTH() (stack_pointer[-4])
#define SET_TOP(v) (stack_pointer[-1] = (v))
#define SET_SECOND(v) (stack_pointer[-2] = (v))
#define SET_THIRD(v) (stack_pointer[-3] = (v))
#define SET_FOURTH(v) (stack_pointer[-4] = (v))
其实ROT_THREE并没有干什么有实际意义的事,所做的就是将栈顶的三个元素来了个乾坤大挪移。ROT_THREE操作完成后的情形如图13所示。
随后的STORE_SUBSCR会将元素插入到PyDictObject对象中去:
[STORE_SUBSCR]
w = TOP();
v = SECOND();
u = THIRD();
STACKADJ(-3);
/* v[w] = u */
PyObject_SetItem(v, w, u);
Py_DECREF(u);
Py_DECREF(v);
Py_DECREF(w);
随着STACKADJ的执行,栈顶指针回退了3格,所以STORE_SUBSCR执行完后,运行时栈里又只剩下了最初由BUILD_MAP创建的PyDictObject对象。到这里,就完成了将一个元素对插入到PyDictObject的操作。剩下的不过是重复上面的动作,将第二个元素对插入到PyDictObject对象中去。最后由我们的老朋友STORE_NAME完成将这个PyDictObject对象添加到f->f_locals中的工作。最后的情形如图14所示:
在成功创建了PyDictObject对象之后,会创建一个非空的PyListObject对象:
l = [1, 2]
# LOAD_CONST 0 (1)
# LOAD_CONST 4 (2)
# BUILD_LIST 2
# STORE_NAME 3 (1)
从这里可以看到,确实Python会首先将会填入PyListObject对象的元素先从consts读入,压入运行时栈,然后如之前描述的,BUILD_LIST再创建PyListObject对象之后,会从栈中依次将元素读出,从后至前地填入PyListObject对象中。
到了这里,对于一般的声明语句或常量赋值表达式,我们都已经了如指掌。下面,通过研究如下的simple2.py,考察变量赋值,变量运算以及最基本的print操作,完成对一般Python语句的考察:
[simple2.py]
a = 5
b = a
c = a + b
print c
编译完成后,其co_consts和co_names如图15所示:
第一条Python代码现在我们已经很熟悉了,不用再费唇舌。现在看第二条,变量间的赋值:
b = a
# LOAD_NAME 0 (a)
# STORE_NAME 1 (b)
对于STORE_NAME,已经摸清楚它到底干了些什么。而对于LOAD_NAME,我们还是第一次遇到:
[LOAD_NAME]
w = GETITEM(names, oparg);
if ((v = f->f_locals) == NULL) {
//report error
break;
}
if (PyDict_CheckExact(v)) {
x = PyDict_GetItem(v, w); //[1]
Py_XINCREF(x);
}
else {
x = PyObject_GetItem(v, w);
if (x == NULL && PyErr_Occurred()) {
if (!PyErr_ExceptionMatches(PyExc_KeyError))
break;
PyErr_Clear();
}
}
if (x == NULL) {
x = PyDict_GetItem(f->f_globals, w); //[2]
if (x == NULL) {
x = PyDict_GetItem(f->f_builtins, w); //[3]
if (x == NULL) {
format_exc_check_arg(
PyExc_NameError,
NAME_ERROR_MSG ,w);
break;
}
}
Py_INCREF(x);
}
PUSH(x);
首先,会从names中抽取出第0个元素,参考前面的simple2.py编译的结果,可知是一个PyStringObject对象“a”。然后检查当前PyFrameObject中所维护的局部变量集合f->f_locals,如果这个东西不存在,那么直接返回(why?),如果局部变量集合存在,就会进行一系列的搜索动作:
[1]:在局部变量集合f_locals中搜索“a”。
[2]:如果f_locals中没有“a”,则在全局变量集合f_globals中搜索“a”。
[3]:如果f_globals中没有“a”,则在Python运行时内建变量集合f_builtins中搜索“a”。
如果搜索到了与“a”对应的元素,那么就将该元素返回。在simple2.py这个例子中,第一条Python代码的执行会在f_locals中插入(“a”,1)的元素对,所以这里会返回PyIntObject对象1。
如果到了最后还搜索不到“a”,那么表示有错误发生,程序引用了一个不存在的符号。
这样的行为正是Python官方文档中所描述的变量的搜索会沿着局部作用域,全局作用域,内建作用域依次上溯,直至搜索成功或全部搜完三个作用域。
现在很清楚了,在b = a执行完成后,内存中的情形如图16所示:
而从前面我们对PyIntObject对象的分析可知,这两个1实际上指向内存中的同一个PyIntObject对象。
现在a,b都是合法而有效的变量了,它们的结合就变得很有趣了:
c = a + b
# LOAD_NAME 0 (a)
# LOAD_NAME 1 (b)
# BINARY_ADD
# STORE_NAME 2 (c)
首先会将a和b所对应的变量值从f_locals中读取出来,压入运行时栈,然后通过BINARY_ADD计算两个变量的和,假设结果为num,最后就通过STORE_NAME,将(“c”,num)元素对插入到f_locals中。过程非常清晰,而现在我们感兴趣的就是那个从两个现有对象创造出新的对象的BINARY_ADD:
[BINARY_ADD]
w = POP();
v = TOP();
if (PyInt_CheckExact(v) && PyInt_CheckExact(w)) {
/* INLINE: int + int */
register long a, b, i;
a = PyInt_AS_LONG(v);
b = PyInt_AS_LONG(w);
i = a + b;
if ((i^a) < 0 && (i^b) < 0)
goto slow_add;
x = PyInt_FromLong(i);
}
else if (PyString_CheckExact(v) &&
PyString_CheckExact(w)) {
x = string_concatenate(v, w, f, next_instr);
/* string_concatenate consumed the ref to v */
goto skip_decref_vx;
}
else {
slow_add:
x = PyNumber_Add(v, w);
}
Py_DECREF(v);
skip_decref_vx:
Py_DECREF(w);
SET_TOP(x);
if (x != NULL) continue;
break;
如果参与运算的两个对象都是PyIntObject对象,会直接将PyIntObject中的value提取出来相加,然后根据相加的结果创建新的PyIntObject对象作为结果返回;同样,如果是PyStringObject对象,也会选择string_concatenate加快速度。如果参与运算的对象落在了这两种有加速机制的情况之外,那么很不幸,只能通过PyNumber_Add完成运算,PyNumber_Add会进行大量的类型判断,寻找操作函数等额外工作,速度会比前两种加速机制慢上很多。一般来说,Python在PyNumber_Add中会首先检查PyNumberMethods中的nb_add能否完成在v和w上的加法运算,如果不能,还会检查PySequenceMethods中的sq_concat能否完成,如果都不能,Python也是有心杀敌,无力回天了,那也只好报告错误了。
可以看到,Python2.4.1在这里假设了用户在使用Python时,大量进行的加法操作是整数的加法和字符串的连接。其实如果你的程序中涉及到了大量的浮点运算,那你完全可以修改BINARY_ADD的代码,为浮点加法运算建立快速通道。实际上,对于加,减,乘,除这些操作,你都可以根据自己程序的实际情况对Python的代码进行改动,这样的改动应该对提升程序运行效率有很大的好处。
最后看一看print的动作,前面分析的byte code都是Python自己跟自己玩,这个print则是Python跟咱们用户玩,也是Python中最常用和易用的用户交互方式:
print c
# LOAD_NAME 2 (c)
# PRINT_ITEM
# PRINT_NEWLINE
其中LOAD_NAME首先将刚才获得的那个加法运算的和c从f_locals中读取出来,压入运行时栈,然后通过PRINT_ITEM完成输出的魔法:
[PRINT_ITEM]
v = POP();
if (stream == NULL || stream == Py_None) {
w = PySys_GetObject("stdout");
}
Py_XINCREF(w);
if (w != NULL && PyFile_SoftSpace(w, 0))
err = PyFile_WriteString(" ", w);
if (err == 0)
err = PyFile_WriteObject(v, w, Py_PRINT_RAW);
……
stream = NULL;
实际上输出的时候会进行很多动作,但是在这里,我们只考虑其大致的流程。首先会将待输出的对象从运行时栈中取出。然后,判断如果stream为NULL,则将w设为标准输出流。这个stream是什么东西呢?它实际上也是一个PyObject对象:
PyObject *stream = NULL; /* for PRINT opcodes */
如果输出的时候,是通过如下的Python代码:print >> file, “str”
那么所产生的byte code中还有一个PRINT_ITEM_TO:
[PRINT_ITEM_TO]
w = stream = POP();
显然,这时在运行时栈中,在待输出对象之前,还会有一个对象,即输出的目标。在执行PRINT_ITEM_TO时,输出的目标就赋给了stream,同时也赋给了w。所以实际上stream是作为一个判断条件来使用的,真正使用的输出目标是w。要多使用这一个stream的原因是w在别的byte code中可能还会使用到,所以无法通过判断w是否为NULL来确定是否输出到标准输出流。
可以看到,在PRINT_ITEM最后,又将stream设为了NULL,又可以为下次输出时的判断做准备了。
在获得了输出的目标和待输出的对象后,PRINT_ITEM将通过PyFile_WriteObject -> PyObject_Print -> internal_print的调用序列最终调用v->ob_type->tp_print,即待输出对象自身所携带的输出函数进行输出。如果对象没有定义tp_print,那么会先调用tp_str或tp_repr获得对象的字符串表示形式,将字符串输出,如果这也失败了,Python也无能为力了,只好返回失败信息。
[绝对原创,转载请注明出处]
Python源码剖析
——Python执行引擎之一般表达式(1)
本文作者: Robert Chen([email protected] )
1 Declare.py
[declare.py]
i = 1
# LOAD_CONST 0
# STORE_NAME 0
s = "Python"
# LOAD_CONST 1
# STORE_NAME 1
d = {}
# BUILD_MAP 0
# STORE_NAME 2
l = []
# BUILD_LIST 0
# STORE_NAME 3
# LOAD_CONST 2
# RETURN_VALUE none
对于declare.py,我们可以解析其生成的pyc文件,解析的结果如图4所示,看一看co_consts和co_names究竟都有些什么:
这些就是declare.py中关于程序运行的重要信息。这些信息在byte code的执行过程中必不可少,在随后的描述中可以看得很清楚。
PyEval_EvalFram中还定义了一些宏,这些宏包括对栈的各种操作以及对tuple元素的访问操作,在执行byte code时,会大量使用这些宏。
#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 BASIC_POP() (*–stack_pointer)
#define PUSH(v) BASIC_PUSH(v)
#define POP() BASIC_POP()
#define STACKADJ(n) BASIC_STACKADJ(n)
我们首先来看一看对第一行Python代码的执行:
i = 1
# LOAD_CONST 0 (1)
# STORE_NAME 0 (i)
其中红色的部分表示当前的字节码指令对源文件中的哪个符号或常量进行了操作,或产生了影响。
在开始之前,我们先通过图5观察一下内存中栈以及f->f_locals的情况,前面说了,f->f_locals将存储程序执行过程中的局部变量,实际也就是Python执行引擎的局部变量表,也是一举足轻重的主儿:)
对于LOAD_CONST,执行引擎的动作如下
[LOAD_CONST]
x = GETITEM(consts, oparg);
Py_INCREF(x);
PUSH(x);
其中,GETITEM(consts, oparg)显然就是GETITEM(consts, 0)。LOAD_CONST的意图很明显,就是从consts中读取一个元素,然后将其压入到Python的运行时栈中。LOAD_CONST完成后的情况如图6所示:
LOAD_CONST只改变了运行时栈,而对f->f_locals没有任何改变。这个对f->f_locals的改变将在STORE_NAME中完成:
[STORE_NAME]
w = GETITEM(names, oparg);
v = POP();
if ((x = f->f_locals) != NULL)
{
if (PyDict_CheckExact(x))
{
PyDict_SetItem(x, w, v);
}
else
{
PyObject_SetItem(x, w, v);
}
Py_DECREF(v);
}
在这里,我们只考虑f->f_locals确实是PyDictObject对象的情况。STORE_NAME首先从names中读取一个元素,作为变量名,然后将刚才LOAD_CONST读取的元素作为变量值,将(变量名,变量值)元素对添加到f->f_locals中。
好了,到了这里,可以很清晰地看到Python代码中变量名与变量值在内存中是通过怎样的一种方式捆绑在一起的了。LOAD_CONST完成后的情况如图7所示。
现在,declare.py中第一行代码执行完毕,第2行代码所产生的byte code实际上跟第一行代码是一样的,只是操作的参数不同了:
s = "Python"
# LOAD_CONST 1 (‘Python’)
# STORE_NAME 1 (s)
图8展示了这段byte code执行时栈和f->f_locals的动态变化:
在declare.py的第三行,我们见到了一点新鲜的东西,在这里,我们并不是简单地Load了,而是凭空创建了一个PyDictObject对象:
d = {}
# BUILD_MAP 0
# STORE_NAME 2 (d)
对于BUILD_MAP,Python运行时会创建一个空的PyDictObject对象,并把这个对象压入到运行时堆栈中:
[BUILD_MAP]
x = PyDict_New();
PUSH(x);
发现了吗?这里有一件很奇怪的事。从Python源代码编译出的字节码中,可以发现,BUILD_MAP是一个带有参数的byte code,从opcode.h中也能证实这一点,但是在这里,我们看到根本没有使用这个参数,可能,这又是“历史遗留”问题 :)
接下来的STORE_NAME我们已经非常熟悉了,看到这条byte code,实际上我们就可以看到执行完毕后的情形了,如图9所示:
对于declare.py最后一行Python代码,很奇怪,居然编译出了四条byte code:
l = []
# BUILD_LIST 0
# STORE_NAME 3 (1)
# LOAD_CONST 2 (none)
# RETURN_VALUE
对于BUILD_LIST,猜想上会与BUILD_MAP是一路货色,但是,BUILD_LIST比BUILD_MAP好太多了,它善待了字节码所带的参数,真正利用了这个参数,而不是BUILD_MAP那样,仅仅做个摆设:
[BUILD_LIST]
x = PyList_New(oparg);
if (x != NULL)
{
for (; –oparg >= 0
{
w = POP();
PyList_SET_ITEM(x, oparg, w);
}
PUSH(x);
}
可以看到,如果Python源代码中创建的不是一个空的list,那么在BUILD_LIST之前一定会有许多LOAD_CONST的操作,将元素压入栈中,在真正执行BUILD_LIST时,会一一从栈中弹出元素,加入到创建的PyListObject对象中。这一点我们下面会详细考察。
在执行了接下来的STORE_NAME后,似乎declare.py中指示的所有工作都完成了,最后两行是干嘛的呢?原来Python在执行了一段Code Block后,一定要返回一点东西,这两行byte code就是用来返回某些东西的:
[RETURN_VALUE]
retval = POP();
why = WHY_RETURN;
实际的返回值在retval中,是从栈中取得的,所以RETURN_VALUE前的那条LOAD_CONST就很清楚了,这家伙将返回值压入了栈了。可以看到,压入栈中的返回值是一个NoneObject,实际上什么有价值的东西也没有返回,但这个过场还是要走的,不走,人民是不答应的:)
执行完整个declare.py的瞬间,栈已经变空了,而所有有用的信息都已经到了f->f_locals的掌握之中,如图10所示:
[绝对原创,转载请注明出处]
Python源码剖析
——Python执行引擎之框架
本文作者: Robert Chen([email protected] )
Python执行引擎是Python的核心,Python源代码在被编译为byte code之后,就将由Python的执行引擎接手整个工作,依次读入每一条byte code,在当前的上下文环境中执行这条byte code。如此反复推磨,所有由Python源代码所规定的动作都会如期望一样,一一展开。
在进入Python的执行引擎之前,我们先来看一看在x86的机器上,程序是以一种什么方式运行的,在这里,我们主要关注运行时栈的栈帧,如图1所示:
图1所示的运行时栈的情形可以看作是如下的C代码的运行时情形:
void f(int a, int b)
{
printf("a=%d, b=%d\n", a, b);
}
void g()
{
f(1, 2);
}
int main()
{
g();
}
其中图1绿色部分对应的是g的栈帧,而黄色部分对应的是f的栈帧。对于一个函数而言,其所有的局部变量和操作都在自己的栈帧中完成,而函数之间的调用则通过创建新的栈帧完成,当然,在函数调用发生时,系统会保存上一个栈帧的栈指针esp和帧指针ebp。大致上,这就是可执行文件在x86机器上的运行原理,而Python正是在执行引擎中通过不同的实现方式模拟了这一原理。
前面我们已经知道,Python源代码经过编译之后,所有的byte code以及程序的其他信息都存放在PyCodeObject对象中,那么Python的执行引擎是否就是在这个PyCodeObject对象上进行所有的动作呢?呃,是,又不是。PyCodeObject中包含了最关键的byte code以及关于程序的所有信息。然而有一点,PyCodeObject没有包含,也不可能包含。这就是执行环境。
什么是执行环境呢,考虑下面的一个例子:
i = ‘Python’
def f():
i = 999
print i #1
f()
print i #2
在1和2两个地方,都进行了同样的动作,即print i,显然,它们所对应的字节码是相同的,但是这两条语句的执行效果是不同的。这样的结果正是在执行环境的影响下产生的。在执行1处的print时,执行环境中,i的值为999;而在执行2处的print时,执行环境中i的值为“Python”。这种同样的名字对应不同的值,甚至不同的类型的情况,必须在运行时动态地被捕捉和维护。这些则不可能在PyCodeObject中被静态地存储。
联想到x86运行程序的机理,我们可以这样来考虑。当Python调用函数f时,会在当前的运行环境之外重新创建一个新的运行环境,在这个新的运行环境中,有一个新的名字为“i”的对象,这个新的运行环境实际上可以看成是一个新的栈帧。所以在Python真正执行的时候,它的执行引擎实际上面对的并不是一个PyCodeObject对象,而是另一个家伙——PyFrameObject,这个东西就是Python对栈帧的模拟,你看,它的名字中还有个Frame呢。
当然,对于Python而言,PyFrameObject对象不是一个我们在x86机器上看到的那个简简单单的栈帧,它实际上包含了其他更多的信息,请看:
[frameobject.h]
typedef struct _frame {
PyObject_VAR_HEAD
struct _frame *f_back; /* previous frame, or NULL */
PyCodeObject *f_code; /* code segment */
PyObject *f_builtins; /* builtin symbol table (PyDictObject) */
PyObject *f_globals; /* global symbol table (PyDictObject) */
PyObject *f_locals; /* local symbol table (any mapping) */
PyObject **f_valuestack; /* points after the last local */
PyObject **f_stacktop;
……
int f_lasti; /* Last instruction if called */
/* As of 2.3 f_lineno is only valid when tracing is active (i.e. when
f_trace is set) — at other times use PyCode_Addr2Line instead. */
int f_lineno; /* Current line number */
int f_nlocals; /* number of locals */
int f_ncells;
int f_nfreevars;
int f_stacksize; /* size of value stack */
PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */
} PyFrameObject;
从f_back我们可以窥见一点,在Python实际的执行中,会产生很多PyFrameObject对象,而这些对象会被链接起来,形成一个链表。是不是看出点眉目来了,这似乎正是对x86机器上栈帧间关系的模拟,在x86上,栈帧间通过esp指针和ebp指针建立了关系,使新的栈帧在结束之后能顺利回退到旧的栈帧中,而Python似乎正是利用一个指针来完成这个动作。那真实的情况是不是这样呢,这是后话,暂且按下不表J
在f_code中存放的是一个待执行的PyCodeObject对象,而接下来的f_builtins,f_globals,f_locals正是动态的执行环境,这三个PyObject*都会指向PyDictObject对象,而在这些PyDictObject对象中,分别维护了builtin的name,global的name,local的name与对应的值之间的映射关系。想想前面的那段Python代码,在执行print i时,会首先到f_locals中去寻找PyStringObject对象‘i’,找到了之后,将其对应的值取出,并打印出来。
在PyFrameObject的开头,有一个PyObject_VAR_HEAD,这表明PyFrameObject是一个变长的对象,即每次创建的PyFrameObject对象的大小可能是不一样的。这些变动的内存是用来做什么的呢?实际上,每一个PyFrameObject对象都维护了一个PyCodeObject对象,这表明一个PyFrameObject对象和Python源代码中的一段Code是对应的,更准确地说,是和我们在研究PyCodeObject时提到的那个Code Block对应的。而在编译一段Code Block时,会计算出这段Code Block执行过程中所需要的栈空间的大小(注意,这个栈空间才是和x86机器上那个用于函数执行的栈空间相对应的概念),这个栈空间的大小存储在f_stacksize中,而这个栈正是那段变动的内存。因为不同的Code Block所需的栈空间的大小是不同的,所以这决定了PyFrameObject一定有一个PyObject_VAR_HEAD。在PyFrameObject对象所维护的栈中,存储的都是PyObject*,你可能看出来了,这个栈的起始位置是从f_localsplus开始的。呃,其实不完全正确,f_localsplus确实维护了一段变动长度的内存,但是这段内存不光是给栈使用的,而且还有别的家伙也会使用:
[frameobject.c](有删节)
PyFrameObject *
PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals,
PyObject *locals)
{
PyFrameObject *f;
int extras, ncells, nfrees, i;
ncells = PyTuple_GET_SIZE(code->co_cellvars);
nfrees = PyTuple_GET_SIZE(code->co_freevars);
extras = code->co_stacksize + code->co_nlocals + ncells + nfrees;
f = PyObject_GC_NewVar(PyFrameObject, &PyFrame_Type, extras);
f->f_nlocals = code->co_nlocals;
f->f_stacksize = code->co_stacksize;
f->f_ncells = ncells;
f->f_nfreevars = nfrees;
extras = f->f_nlocals + ncells + nfrees;
f->f_valuestack = f->f_localsplus + extras;
f->f_stacktop = f->f_valuestack;
return f;
}
可见,在创建PyFrameObject对象时,额外申请的那部分内存有一部分是给PyCodeObject对象中存储的那些co_names啊,co_freevars啊,co_cellvars啊使用的,而另一部分才是给栈使用的。所以,PyFrameObject对象中的栈的起始位置是由f_valuestack维护的,而f_stacktop维护了当前的栈顶。图2是一个刚被创建的PyFrameObject对象的示意图:
当Python启动后,会首先进行Python运行时环境的初始化,这个过程非常地复杂,我们将在后面用单独的一章来剖析,这里我们架设初始化的动作已经完成,我们已经站在了执行引擎的门槛外,只需要轻轻推动一下第一张骨牌,整个执行过程就像多米诺骨牌一样,一环扣一环地展开。
这个推动第一张骨牌的地方在一个名叫PyEval_EvalFram的函数中,PyEval_EvalFrame首先会初始化一些变量,其中PyFrameObject对象中的PyCodeObject对象包含的重要信息都被照顾到了。当然,另一个重要的动作就是初始化了堆栈的栈顶指针,使其指向f->f_stacktop:
[PyEval_EvalFrame in ceval.c]
co = f->f_code;
names = co->co_names;
consts = co->co_consts;
fastlocals = f->f_localsplus;
freevars = f->f_localsplus + f->f_nlocals;
first_instr = PyString_AS_STRING(co->co_code);
/* An explanation is in order for the next line.
f->f_lasti now refers to the index of the last instruction
executed. You might think this was obvious from the name, but
this wasn’t always true before 2.3! PyFrame_New now sets
f->f_lasti to -1 (i.e. the index *before* the first instruction)
and YIELD_VALUE doesn’t fiddle with f_lasti any more. So this
does work. Promise. */
next_instr = first_instr + f->f_lasti + 1;
stack_pointer = f->f_stacktop;
assert(stack_pointer != NULL);
f->f_stacktop = NULL; /* remains NULL unless yield suspends frame */
前面我们说过,在PyCodeObject对象的co_code域中保存着字节码和字节码的参数,Python执行引擎执行字节码的过程就是从头到尾遍历整个co_code域,依次执行字节码的过程。在Python的执行引擎中,利用三个变量来完成整个遍历过程,图3展示了这三个变量在遍历中某时刻的情形:
那么这个一步一步的动作是如何完成的呢,我们来看一看Python执行引擎执行字节码的整体架构,其实就是一个for循环加上一个巨大的switch/case结构,熟悉Windows编程的朋友可以想象一下Windows下那个巨大的消息循环,没错,就是那样的结构:
[ceval.c]
/* Interpreter main loop */
PyObject* PyEval_EvalFrame(PyFrameObject *f)
{
……
why = WHY_NOT;
for (;;) {
……
fast_next_opcode:
f->f_lasti = INSTR_OFFSET();
/* Extract opcode and argument */
opcode = NEXTOP();
oparg = 0; /* allows oparg to be stored in a register because
it doesn’t have to be remembered across a full loop */
if (HAS_ARG(opcode))
oparg = NEXTARG();
dispatch_opcode:
switch (opcode) {
case NOP:
goto fast_next_opcode;
case LOAD_FAST:
……
}
}
注意,这只是一个极其简化之后Python执行引擎的样子,如果想看Python执行引擎的尊容,请参考ceval.c中的源码。
在这个执行架构中,对字节码的一步一步地遍历正是通过几个宏来实现的:
[ceval.c]
#define INSTR_OFFSET() (next_instr – first_instr)
#define NEXTOP() (*next_instr++)
#define NEXTARG() (next_instr += 2, (next_instr[-1]<<8) + next_instr[-2])
在对PyCodeObject对象的分析中我们说过,Python的字节码有的是带参数的,有的是没有参数的,而判断是否是带参字节码是通过HAS_ARG这个宏实现的。注意对不同的字节码,next_instr的位移是不同的,但是无论如何,next_instr总是指向Python下一条要执行的字节码,很像x86机器中的那个PC寄存器,对吗?
需要提到的一点是那个名叫why的神秘变量,这个家伙指示了在退出这个巨大的for循环时Python执行引擎的状态。你知道,世事难料,Python执行引擎不一定每次执行都会寿终正寝,很有可能在执行到某条字节码的时候,产生了错误,没错,就是我们熟悉的那个“异常”,exception。这个时候,就需要知道Python执行引擎到底是因为什么原因结束了对字节码的执行,是正常结束呢,还是因为有错误发生,实在是活不下去了,why义无反顾地负担起这一重任。
变量why的取值范围在ceval.c中被定义,其实也就是Python结束字节码执行时的状态:
[ceval.c]
/* Status code for main loop (reason for stack unwind) */
enum why_code {
WHY_NOT = 0×0001, /* No error */
WHY_EXCEPTION = 0×0002, /* Exception occurred */
WHY_RERAISE = 0×0004, /* Exception re-raised by ‘finally’ */
WHY_RETURN = 0×0008, /* ‘return’ statement */
WHY_BREAK = 0×0010, /* ‘break’ statement */
WHY_CONTINUE = 0×0020, /* ‘continue’ statement */
WHY_YIELD = 0×0040 /* ‘yield’ operator */
};
好了,到现在,想必你已经对Python的执行引擎的大体框架了然于胸了。当Python的执行流程进入了PyEval_EvalFrame中的那个for循环,取出第一条字节码之后,第一张多米诺骨牌已经被推倒,命运不可阻挡地降临了。一条接一条的字节码像潮水一样汹涌而来,浩浩荡荡。天玄地黄,长风浩荡,我们展开双臂,迎接那些在天地间游荡的,那些在最平凡的面孔下蕴藏着最惊人的力量的精灵——字节码。
[绝对原创 转载请注明出处]
Python源码剖析
——Pyc文件解析
本文作者: Robert Chen ([email protected] )
通常认为,Python是一种解释性的语言,但是这种说法是不正确的,实际上,Python在执行时,首先会将.py文件中的源代码编译成Python的byte code(字节码),然后再由Python Virtual Machine来执行这些编译好的byte code。这种机制的基本思想跟Java,.NET是一致的。然而,Python Virtual Machine与Java或.NET的Virtual Machine不同的是,Python的Virtual Machine是一种更高级的Virtual Machine。这里的高级并不是通常意义上的高级,不是说Python的Virtual Machine比Java或.NET的功能更强大,更拽,而是说和Java或.NET相比,Python的Virtual Machine距离真实机器的距离更远。或者可以这么说,Python的Virtual Machine是一种抽象层次更高的Virtual Machine。
我们来考虑下面的Python代码:
[demo.py]
class A:
pass
def Fun():
pass
value = 1
str = “Python”
a= A()
Fun()
Python在执行CodeObject.py时,首先需要进行的动作就是对其进行编译,编译的结果是什么呢?当然有字节码,否则Python也就没办法在玩下去了。然而除了字节码之外,还包含其它一些结果,这些结果也是Python运行的时候所必需的。看一下我们的demo.py,用我们的眼睛来解析一下,从这个文件中,我们可以看到,其中包含了一些字符串,一些常量值,还有一些操作。当然,Python对操作的处理结果就是自己码。那么Python的编译过程对字符串和常量值的处理结果是什么呢?实际上,这些在Python源代码中包含的静态的信息都会被Python收集起来,编译的结果中包含了字符串,常量值,字节码等等在源代码中出现的一切有用的静态信息。而这些信息最终会被存储在Python运行期的一个对象中,当Python运行结束后,这些信息甚至还会被存储在一种文件中。这个对象和文件就是我们这章探索的重点:PyCodeObject对象和Pyc文件。
可以说,PyCodeObject就是Python源代码编译之后的关于程序的静态信息的集合:
[compile.h]
/* Bytecode object */
typedef struct {
PyObject_HEAD
int co_argcount; /* #arguments, except *args */
int co_nlocals; /* #local variables */
int co_stacksize; /* #entries needed for evaluation stack */
int co_flags; /* CO_..., see below */
PyObject*co_code; /* instruction opcodes */
PyObject*co_consts; /* list (constants used) */
PyObject*co_names; /* list of strings (names used) */
PyObject*co_varnames; /* tuple of strings (local variable names) */
PyObject*co_freevars; /* tuple of strings (free variable names) */
PyObject*co_cellvars; /* tuple of strings (cell variable names) */
/* The rest doesn't count for hash/cmp */
PyObject*co_filename; /* string (where it was loaded from) */
PyObject*co_name; /* string (name, for reference) */
int co_firstlineno; /* first source line number */
PyObject*co_lnotab; /* string (encoding addr<->lineno mapping) */
}PyCodeObject;
在对Python源代码进行编译的时候,对于一段Code(Code Block),会创建一个PyCodeObject与这段Code对应。那么如何确定多少代码算是一个Code Block呢,事实上,当进入新的作用域时,就开始了新的一段Code。也就是说,对于下面的这一段Python源代码:
[CodeObject.py]
class A:
pass
def Fun():
pass
a= A()
Fun()
在Python编译完成后,一共会创建3个PyCodeObject对象,一个是对应CodeObject.py的,一个是对应class A这段Code(作用域),而最后一个是对应def Fun这段Code的。每一个PyCodeObject对象中都包含了每一个代码块经过编译后得到的byte code。但是不幸的是,Python在执行完这些byte code后,会销毁PyCodeObject,所以下次再次执行这个.py文件时,Python需要重新编译源代码,创建三个PyCodeObject,然后执行byte code。
很不爽,对不对?Python应该提供一种机制,保存编译的中间结果,即byte code,或者更准确地说,保存PyCodeObject。事实上,Python确实提供了这样一种机制——Pyc文件。
Python中的pyc文件正是保存PyCodeObject的关键所在,我们对Python解释器的分析就从pyc文件,从pyc文件的格式开始。
在分析pyc的文件格式之前,我们先来看看如何产生pyc文件。在执行一个.py文件中的源代码之后,Python并不会自动生成与该.py文件对应的.pyc文件。我们需要自己触发Python来创建pyc文件。下面我们提供一种使Python创建pyc文件的方法,其实很简单,就是利用Python的import机制。
在Python运行的过程中,如果碰到import abc,这样的语句,那么Python将到设定好的path中寻找abc.pyc或abc.dll文件,如果没有这些文件,而只是发现了abc.py,那么Python会首先将abc.py编译成相应的PyCodeObject的中间结果,然后创建abc.pyc文件,并将中间结果写入该文件。接下来,Python才会对abc.pyc文件进行一个import的动作,实际上也就是将abc.pyc文件中的PyCodeObject重新在内存中复制出来。了解了这个过程,我们很容易利用下面所示的generator.py来创建上面那段代码(CodeObjectt.py)对应的pyc文件了。
generator.py |
CodeObject.py |
import test print"Done"
|
class A: pass
def Fun(): pass
a= A() Fun() |
图1所示的是Python产生的pyc文件:
可以看到,pyc是一个二进制文件,那么Python如何解释这一堆看上去毫无意义的字节流就至关重要了。这也就是pyc文件的格式。
要了解pyc文件的格式,首先我们必须要清楚PyCodeObject中每一个域都表示什么含义,这一点是无论如何不能绕过去的。
Field |
Content |
co_argcount |
Code Block的参数的个数,比如说一个函数的参数 |
co_nlocals |
Code Block中局部变量的个数 |
co_stacksize |
执行该段Code Block需要的栈空间 |
co_flags |
N/A |
co_code |
Code Block编译所得的byte code。以PyStringObject的形式存在 |
co_consts |
PyTupleObject对象,保存该Block中的常量 |
co_names |
PyTupleObject对象,保存该Block中的所有符号 |
co_varnames |
N/A |
co_freevars |
N/A |
co_cellvars |
N/A |
co_filename |
Code Block所对应的.py文件的完整路径 |
co_name |
Code Block的名字,通常是函数名或类名 |
co_firstlineno |
Code Block在对应的.py文件中的起始行 |
co_lnotab |
byte code与.py文件中source code行号的对应关系,以PyStringObject的形式存在 |
需要说明一下的是co_lnotab域。在Python2.3以前,有一个byte code,唤做SET_LINENO,这个byte code会记录.py文件中source code的位置信息,这个信息对于调试和显示异常信息都有用。但是,从Python2.3之后,Python在编译时不会再产生这个byte code,相应的,Python在编译时,将这个信息记录到了co_lnotab中。
co_lnotab中的byte code和source code的对应信息是以unsigned bytes的数组形式存在的,数组的形式可以看作(byte code在co_code中位置增量,代码行数增量)形式的一个list。比如对于下面的例子:
Byte code在co_code中的偏移 |
.py文件中源代码的行数 |
0 |
1 |
6 |
2 |
50 |
7 |
这里有一个小小的技巧,Python不会直接记录这些信息,相反,它会记录这些信息间的增量值,所以,对应的co_lnotab就应该是:0,1, 6,1, 44,5。
前面我们提到,Python在import时,如果没有找到相应的pyc文件或dll文件,就会在py文件的基础上自动创建pyc文件。那么,要想了解pyc的格式到底是什么样的,我们只需要考察Python在将编译得到的PyCodeObject写入到pyc文件中时到底进行了怎样的动作就可以了。下面的函数就是我们的切入点:
[import.c]
static void write_compiled_module(PyCodeObject*co, char *cpathname, long mtime)
{
FILE*fp;
fp=open_exclusive(cpathname);
PyMarshal_WriteLongToFile(pyc_magic, fp, Py_MARSHAL_VERSION);
/* First write a 0 for mtime */
PyMarshal_WriteLongToFile(0L, fp, Py_MARSHAL_VERSION);
PyMarshal_WriteObjectToFile((PyObject*)co, fp, Py_MARSHAL_VERSION);
/* Now write the true mtime */
fseek(fp, 4L, 0);
PyMarshal_WriteLongToFile(mtime, fp, Py_MARSHAL_VERSION);
fflush(fp);
fclose(fp);
}
这里的cpathname当然是pyc文件的绝对路径。首先我们看到会将pyc_magic这个值写入到文件的开头。实际上,pyc_magic对应一个MAGIC的值。MAGIC是用来保证Python兼容性的一个措施。比如说要防止Python2.4的运行环境加载由Python1.5产生的pyc文件,那么只需要将Python2.4和Python1.5的MAGIC设为不同的值就可以了。Python在加载pyc文件时会首先检查这个MAGIC值,从而拒绝加载不兼容的pyc文件。那么pyc文件为什么会不兼容了,一个最主要的原因是byte code的变化,由于Python一直在不断地改进,有一些byte code退出了历史舞台,比如上面提到的SET_LINENO;或者由于一些新的语法特性会加入新的byte code,这些都会导致Python的不兼容问题。
pyc文件的写入动作最后会集中到下面所示的几个函数中(这里假设代码只处理写入到文件,即p->fp是有效的。因此代码有删减,另有一个w_short未列出。缺失部分,请参考Python源代码):
[marshal.c]
typedef struct {
FILE*fp;
int error;
int depth;
PyObject*strings;/* dict on marshal, list on unmarshal */
}WFILE;
#define w_byte(c, p) putc((c), (p)->fp)
static void w_long(long x, WFILE*p)
{
w_byte((char)(x &0xff), p);
w_byte((char)((x>>8)&0xff), p);
w_byte((char)((x>>16)&0xff), p);
w_byte((char)((x>>24)&0xff), p);
}
static void w_string(char *s, int n, WFILE*p)
{
fwrite(s, 1, n, p->fp);
}
在调用PyMarshal_WriteLongToFile时,会直接调用w_long,但是在调用PyMarshal_WriteObjectToFile时,还会通过一个间接的函数:w_object。需要特别注意的是PyMarshal_WriteObjectToFile的第一个参数,这个参数正是Python编译出来的PyCodeObject对象。
w_object的代码非常长,这里就不全部列出。其实w_object的逻辑非常简单,就是对应不同的对象,比如string,int,list等,会有不同的写的动作,然而其最终目的都是通过最基本的w_long或w_string将整个PyCodeObject写入到pyc文件中。
对于PyCodeObject,很显然,会遍历PyCodeObject中的所有域,将这些域依次写入:
[marshal.c]
static void w_object(PyObject*v, WFILE*p)
{
……
else if (PyCode_Check(v))
{
PyCodeObject*co=(PyCodeObject*)v;
w_byte(TYPE_CODE, p);
w_long(co->co_argcount, p);
w_long(co->co_nlocals, p);
w_long(co->co_stacksize, p);
w_long(co->co_flags, p);
w_object(co->co_code, p);
w_object(co->co_consts, p);
w_object(co->co_names, p);
w_object(co->co_varnames, p);
w_object(co->co_freevars, p);
w_object(co->co_cellvars, p);
w_object(co->co_filename, p);
w_object(co->co_name, p);
w_long(co->co_firstlineno, p);
w_object(co->co_lnotab, p);
}
……
}
而对于一个PyListObject对象,想象一下会有什么动作?没错,还是遍历!!!:
[w_object() in marshal.c]
……
else if (PyList_Check(v))
{
w_byte(TYPE_LIST, p);
n=PyList_GET_SIZE(v);
w_long((long)n, p);
for (i=0;i<n;i++)
{
w_object(PyList_GET_ITEM(v, i), p);
}
}
……
而如果是PyIntObject,嗯,那太简单了,几乎没有什么可说的:
[w_object() in marshal.c]
……
else if (PyInt_Check(v))
{
w_byte(TYPE_INT, p);
w_long(x, p);
}
……
有没有注意到TYPE_LIST,TYPE_CODE,TYPE_INT这样的标志?pyc文件正是利用这些标志来表示一个新的对象的开始,当加载pyc文件时,加载器才能知道在什么时候应该进行什么样的加载动作。这些标志同样也是在import.c中定义的:
[import.c]
#define TYPE_NULL '0'
#define TYPE_NONE 'N'
。。。。。。
#define TYPE_INT 'i'
#define TYPE_STRING 's'
#define TYPE_INTERNED 't'
#define TYPE_STRINGREF 'R'
#define TYPE_TUPLE '('
#define TYPE_LIST '['
#define TYPE_CODE 'c'
到了这里,可以看到,Python对于中间结果的导出实际是不复杂的。实际上在write的动作中,不论面临PyCodeObject还是PyListObject这些复杂对象,最后都会归结为简单的两种形式,一个是对数值的写入,一个是对字符串的写入。上面其实我们已经看到了对数值的写入过程。在写入字符串时,有一套比较复杂的机制。在了解字符串的写入机制前,我们首先需要了解一个写入过程中关键的结构体WFILE(有删节):
[marshal.c]
typedef struct {
FILE*fp;
int error;
int depth;
PyObject*strings;/* dict on marshal, list on unmarshal */
}WFILE;
这里我们也只考虑fp有效,即写入到文件,的情况。WFILE可以看作是一个对FILE*的简单包装,但是在WFILE里,出现了一个奇特的strings域。这个域是在pyc文件中写入或读出字符串的关键所在,当向pyc中写入时,string会是一个PyDictObject对象;而从pyc中读出时,string则会是一个PyListObject对象。
[marshal.c]
void PyMarshal_WriteObjectToFile(PyObject*x, FILE*fp, int version)
{
WFILEwf;
wf.fp=fp;
wf.error=0;
wf.depth=0;
wf.strings = (version > 0) ? PyDict_New() : NULL;
w_object(x, &wf);
}
可以看到,strings在真正开始写入之前,就已经被创建了。在w_object中对于字符串的处理部分,我们可以看到对strings的使用:
[w_object() in marshal.c]
……
else if (PyString_Check(v))
{
if (p->strings&&PyString_CHECK_INTERNED(v))
{
PyObject*o=PyDict_GetItem(p->strings, v);
if (o)
{
long w=PyInt_AsLong(o);
w_byte(TYPE_STRINGREF, p);
w_long(w, p);
goto exit;
}
else
{
o=PyInt_FromLong(PyDict_Size(p->strings));
PyDict_SetItem(p->strings, v, o);
Py_DECREF(o);
w_byte(TYPE_INTERNED, p);
}
}
else
{
w_byte(TYPE_STRING, p);
}
n=PyString_GET_SIZE(v);
w_long((long)n, p);
w_string(PyString_AS_STRING(v), n, p);
}
……
真正有趣的事发生在这个字符串是一个需要被进行INTERN操作的字符串时。可以看到,WFILE的strings域实际上是一个从string映射到int的一个PyDictObject对象。这个int值是什么呢,这个int值是表示对应的string是第几个被加入到WFILE.strings中的字符串。
这个int值看上去似乎没有必要,记录一个string被加入到WFILE.strings中的序号有什么意义呢?好,让我们来考虑下面的情形:
假设我们需要向pyc文件中写入三个string:”Jython”, “Ruby”, “Jython”,而且这三个string都需要被进行INTERN操作。对于前两个string,没有任何问题,闭着眼睛写入就是了。完成了前两个string的写入后,WFILE.strings与pyc文件的情况如图2所示:
在写入第三个字符串的时候,麻烦来了。对于这个“Jython”,我们应该怎么处理呢?
是按照上两个string一样吗?如果这样的话,那么写入后,WFILE.strings和pyc的情况如图3所示:
我们可以不管WFILE.strings怎么样了,但是一看pyc文件,我们就知道,问题来了。在pyc文件中,出现了重复的内容,关于“Jython”的信息重复了两次,这会引起什么麻烦呢?想象一下在python代码中,我们创建了一个button,在此之后,多次使用了button,这样,在代码中,“button”将出现多次。想象一下吧,我们的pyc文件会变得多么臃肿,而其中充斥的只是毫无价值的冗余信息。如果你是Guido,你能忍受这样的设计吗?当然不能!!于是Guido给了我们TYPE_STRINGREF这个东西。在解析pyc文件时,这个标志表明后面的一个数值表示了一个索引值,根据这个索引值到WFILE.strings中去查找,就能找到需要的string了。
有了TYPE_STRINGREF,我们的pyc文件就能变得苗条了,如图4所示:
看一下加载pyc文件的过程,我们就能对这个机制更加地明了了。前面我们提到,在读入pyc文件时,WFILE.strings是一个PyListObject对象,所以在读入前两个字符串后,WFILE.strings的情形如图5所示:
在加载紧接着的(R,0)时,因为解析到是一个TYPE_STRINGREF标志,所以直接以标志后面的数值0位索引访问WFILE.strings,立刻可得到字符串“Jython”。
到了这里,关于PyCodeObject与pyc文件,我们只剩下最后一个有趣的话题了。还记得前面那个test.py吗?我们说那段简单的什么都做不了的python代码就要产生三个PyCodeObject。而在write_compiled_module中我们又亲眼看到,Python运行环境只会对一个PyCodeObject对象调用PyMarshal_WriteObjectToFile操作。刹那间,我们竟然看到了两个遗失的PyCodeObject对象。
Python显然不会犯这样低级的错误,想象一下,如果你是Guido,这个问题该如何解决?首先我们会假想,有两个PyCodeObject对象一定是包含在另一个PyCodeObject中的。没错,确实如此,还记得我们最开始指出的Python是如何确定一个Code Block的吗?对喽,就是作用域。仔细看一下test.py,你会发现作用域呈现出一种嵌套的结构,这种结构也正是PyCodeObject对象之间的结构。所以到现在清楚了,与Fun和A对应得PyCodeObject对象一定是包含在与全局作用域对应的PyCodeObject对象中的,而PyCodeObject结构中的co_consts域正是这两个PyCodeObject对象的藏身之处,如图6所示:
在对一个PyCodeObject对象进行写入到pyc文件的操作时,如果碰到它包含的另一个PyCodeObject对象,那么就会递归地执行写入PyCodeObject对象的操作。如此下去,最终所有的PyCodeObject对象都会被写入到pyc文件中去。而且pyc文件中的PyCodeObject对象也是以一种嵌套的关系联系在一起的。
Python源代码在执行前会被编译为Python的byte code,Python的执行引擎就是根据这些byte code来进行一系列的操作,从而完成对Python程序的执行。在Python2.4.1中,一共定义了103条byte code:
[opcode.h]
#define STOP_CODE 0
#define POP_TOP 1
#define ROT_TWO 2
……
#define CALL_FUNCTION_KW 141
#define CALL_FUNCTION_VAR_KW 142
#define EXTENDED_ARG 143
所有这些字节码的操作含义在Python自带的文档中有专门的一页进行描述,当然,也可以到下面的网址察看:http://docs.python.org/lib/bytecodes.html。
细心的你一定发现了,byte code的编码却到了143。没错,Python2.4.1中byte code的编码并没有按顺序增长,比如编码为5的ROT_FOUR之后就是编码为9的NOP。这可能是历史遗留下来的,你知道,在咱们这行,历史问题不是什么好东西,搞得现在还有许多人不得不很郁闷地面对MFC :)
Python的143条byte code中,有一部分是需要参数的,另一部分是没有参数的。所有需要参数的byte code的编码都大于或等于90。Python中提供了专门的宏来判断一条byte code是否需要参数:
[opcode.h]
#define HAS_ARG(op) ((op) >= HAVE_ARGUMENT)
好了,到了现在,关于PyCodeObject和pyc文件的一切我们都已了如指掌了,关于Python的现在我们可以做一些非常有趣的事了。呃,在我看来,最有趣的事莫过于自己写一个pyc文件的解析器。没错,利用我们现在所知道的一切,我们真的可以这么做了。图7展现的是对本章前面的那个test.py的解析结果:
更进一步,我们还可以解析byte code。前面我们已经知道,Python在生成pyc文件时,会将PyCodeObject对象中的byte code也写入到pyc文件中,而且这个pyc文件中还记录了每一条byte code与Python源代码的对应关系,嗯,就是那个co_lnotab啦。假如现在我们知道了byte code在co_code中的偏移地址,那么与这条byte code对应的Python源代码的位置可以通过下面的算法得到(Python伪代码):
lineno = addr = 0
for addr_incr, line_incr in c_lnotab:
addr += addr_incr
if addr > A:
return lineno
lineno += line_incr
下面是对一段Python源代码反编译为byte code的结果,这个结果也将作为下一章对Python执行引擎的分析的开始:
i=1
# LOAD_CONST 0
# STORE_NAME 0
s="Python"
# LOAD_CONST 1
# STORE_NAME 1
d={}
# BUILD_MAP 0
# STORE_NAME 2
l=[]
# BUILD_LIST 0
# STORE_NAME 3
# LOAD_CONST 2
# RETURN_VALUE none
再往前想一想,从现在到达的地方出发,实际上我们就可以做出一个Python的执行引擎了,哇,这是多么激动人心的事啊。遥远的天空,一抹朝阳,缓缓升起了……
事实上,Python标准库中提供了对python进行反编译的工具dis,利用这个工具,可以很容易地得到我们在这里得到的结果,当然,还要更详细一些,图8展示了利用dis工具对CodeObject.py进行反编译的结果:
在图8显示的结果中,最左面一列显示的是CodeObject.py中源代码的行数,左起第二列显示的是当前的字节码指令在co_code中的偏移位置。
在以后的分析中,我们大部分将采用dis工具的反编译结果,在有些特殊情况下会使用我们自己的反编译结果。
对于关注《Python源码剖析》的朋友,非常抱歉,最近一直没有更新,主要的原因是由于最近在剖析Python2.3中引入的New Style Class机制时进度变慢了,一方面是因为这个Topic本身的难度,另一方面也是因为做这个工作是在下班之后的空余时间。最近大部分空余时间都被这个New Style Class占据,所以也没有来得及更新《Python源码剖析》,这几天我会尽力提交新的内容上来,请继续关注,谢谢 :)
[绝对原创 转载请注明出处]
Python源码剖析
——Small Python
本文作者: Robert Chen ([email protected] )
在详细考察了Python中最常用的几个对象之后,我们现在完全可以利用这些对象做出一个最简单的Python。这一章的目的就是模拟出一个最简单的Python——Small Python。
在Small Python中,我们首先需要实现之前已经分析过的那些对象,比如PyIntObject,与CPython不同的是,我们并没有实现象CPython那样复杂的机制,作为一个模拟程序,我们只实现了简单的功能,也没有引入对象缓冲池的机制。这一切都是为了简洁而清晰地展示出Python运行时的脉络。
在Small Python中,实际上还需要实现Python的运行时环境,Python的运行时环境是我们在以后的章节中将要剖析的重点。在这里只是展示了其核心的思想——利用PyDictObject对象来维护变量名到变量值的映射。
当然,在CPython中,还有许多其他的主题,比如Python源代码的编译,Python字节码的生成和执行等等,在Small Python中,我们都不会涉及,因为到目前为止,我们没有任何资本做出如此逼真的模拟。不过当我们完成这本书的探索之后,就完全有能力实现一个真正的Python了。
在Small Python中,我们仅仅实现了PyIntObject,PyStringObject以及PyDictObject对象,仅仅实现了加法运算和输出操作。同时编译的过程也被简化到了极致,因此我们的Small Python只能处理非常受限的表达式。虽然很简陋,但从中可以看到Python的骨架,同时,这也是我们深入Python解释器和运行时的起点。
在Small Python中,对象机制与CPython完全相同:
[PyObject]
#define PyObject_HEAD \
int refCount;\
struct tagPyTypeObject *type
#define PyObject_HEAD_INIT(typePtr)\
0, typePtr
typedef struct tagPyObject
{
PyObject_HEAD;
}PyObject;
但是对于类型对象,我们进行了大规模的删减。最终在类型对象中,只定义了加法操作,Hash操作以及输出操作:
[PyTypeObject]
//definition of PyTypeObject
typedef void (*PrintFun)(PyObject* object);
typedef PyObject* (*AddFun)(PyObject* left, PyObject* right);
typedef long (*HashFun)(PyObject* object);
typedef struct tagPyTypeObject
{
PyObject_HEAD;
char* name;
PrintFun print;
AddFun add;
HashFun hash;
}PyTypeObject;
PyIntObject的实现与CPython几乎是一样的,不过没有复杂的对象缓冲机制:
[PyIntObject]
typedef struct tagPyIntObject
{
PyObject_HEAD;
int value;
}PyIntObject;
PyObject* PyInt_Create(int value)
{
PyIntObject* object = new PyIntObject;
object->refCount = 1;
object->type = &PyInt_Type;
object->value = value;
return (PyObject*)object;
}
static void int_print(PyObject* object)
{
PyIntObject* intObject = (PyIntObject*)object;
printf("%d\n", intObject->value);
}
static PyObject* int_add(PyObject* left, PyObject* right)
{
PyIntObject* leftInt = (PyIntObject*)left;
PyIntObject* rightInt = (PyIntObject*)right;
PyIntObject* result = (PyIntObject*)PyInt_Create(0);
if(result == NULL)
{
printf("We have no enough memory!!");
}
else
{
result->value = leftInt->value + rightInt->value;
}
return (PyObject*)result;
}
static long int_hash(PyObject* object)
{
return (long)((PyIntObject*)object)->value;
}
PyTypeObject PyInt_Type =
{
PyObject_HEAD_INIT(&PyType_Type),
"int",
int_print,
int_add,
int_hash
};
Small Python中的PyStringObject与CPython中大不相同,在CPython中,它是一个变长对象,而Small Python中只是一个简单的定长对象,因为Small Python的定位就是个演示的程序:
[PyStrObject]
typedef struct tagPyStrObject
{
PyObject_HEAD;
int length;
long hashValue;
char value[50];
}PyStringObject;
PyObject* PyStr_Create(const char* value)
{
PyStringObject* object = new PyStringObject;
object->refCount = 1;
object->type = &PyString_Type;
object->length = (value == NULL) ? 0 : strlen(value);
object->hashValue = -1;
memset(object->value, 0, 50);
if(value != NULL)
{
strcpy(object->value, value);
}
return (PyObject*)object;
}
static void string_print(PyObject* object)
{
PyStringObject* strObject = (PyStringObject*)object;
printf("%s\n", strObject->value);
}
static long string_hash(PyObject* object)
{
PyStringObject* strObject = (PyStringObject*)object;
register int len;
register unsigned char *p;
register long x;
if (strObject->hashValue != -1)
return strObject->hashValue;
len = strObject->length;
p = (unsigned char *)strObject->value;
x = *p << 7;
while (–len >= 0)
x = (1000003*x) ^ *p++;
x ^= strObject->length;
if (x == -1)
x = -2;
strObject->hashValue = x;
return x;
}
static PyObject* string_add(PyObject* left, PyObject* right)
{
PyStringObject* leftStr = (PyStringObject*)left;
PyStringObject* rightStr = (PyStringObject*)right;
PyStringObject* result = (PyStringObject*)PyStr_Create(NULL);
if(result == NULL)
{
printf("We have no enough memory!!");
}
else
{
strcpy(result->value, leftStr->value);
strcat(result->value, rightStr->value);
}
return (PyObject*)result;
}
PyTypeObject PyString_Type =
{
PyObject_HEAD_INIT(&PyType_Type),
"str",
string_print,
string_add,
string_hash
};
在Python的解释器工作时,还有一个非常重要的对象,PyDictObject对象。PyDictObject对象在Python运行时会维护(变量名,变量值)的映射关系,Python所有的动作都是基于这种映射关系。在Small Python中,我们基于C++中的map来实现PyDictObject对象。当然,map的运行效率比CPython中所采用的hash技术会慢上一些,但是对于我们的Small Python,map就足够了:
[PyDictObject]
typedef struct tagPyDictObject
{
PyObject_HEAD;
map<long, PyObject*> dict;
}PyDictObject;
PyObject* PyDict_Create()
{
//create object
PyDictObject* object = new PyDictObject;
object->refCount = 1;
object->type = &PyDict_Type;
return (PyObject*)object;
}
PyObject* PyDict_GetItem(PyObject* target, PyObject* key)
{
long keyHashValue = (key->type)->hash(key);
map<long, PyObject*>& dict = ((PyDictObject*)target)->dict;
map<long, PyObject*>::iterator it = dict.find(keyHashValue);
map<long, PyObject*>::iterator end = dict.end();
if(it == end)
{
return NULL;
}
return it->second;
}
int PyDict_SetItem(PyObject* target, PyObject* key, PyObject* value)
{
long keyHashValue = (key->type)->hash(key);
PyDictObject* dictObject = (PyDictObject*)target;
(dictObject->dict)[keyHashValue] = value;
return 0;
}
//function for PyDict_Type
static void dict_print(PyObject* object)
{
PyDictObject* dictObject = (PyDictObject*)object;
printf("{");
map<long, PyObject*>::iterator it = (dictObject->dict).begin();
map<long, PyObject*>::iterator end = (dictObject->dict).end();
for( ; it != end; ++it)
{
//print key
printf("%d : ", it->first);
//print value
PyObject* value = it->second;
(value->type)->print(value);
printf(", ");
}
printf("}\n");
}
PyTypeObject PyDict_Type =
{
PyObject_HEAD_INIT(&PyType_Type),
"dict",
dict_print,
0,
0
};
Small Python中的对象机制的所有内容都在上边列出了,非常简单,对吧,这就对了,要的就是这个简单。
说Small Python中没有编译,对的,它根本就不会进行任何常规的编译动作,没有token解析,没有抽象语法树的建立。但说Small Python中有那么一点点编译的味道,其实也不错,我们叫这种动作为解释。无论如何,它至少要解析输入的语句,以判断这条语句到底是要干什么,它是要上山打虎呢,还是要下河摸鱼呢,如果连这最基本的都做不到,Small Python还不如回家卖红薯得了。
然而Small Python中的这种解释动作还是被简化到了极致,它实际上就是简单的字符串查找加if…else…结构:
voidExcuteCommand(string&command)
{
string::size_type pos = 0;
if((pos = command.find("print ")) != string::npos)
{
ExcutePrint(command.substr(6));
}
else if((pos = command.find(" = ")) != string::npos)
{
string target = command.substr(0, pos);
string source = command.substr(pos+3);
ExcuteAdd(target, source);
}
}
voidExcuteAdd(string&target, string&source)
{
string::size_type pos;
if(IsSourceAllDigit(source))
{
PyObject* intValue = PyInt_Create(atoi(source.c_str()));
PyObject* key = PyStr_Create(target.c_str());
PyDict_SetItem(m_LocalEnvironment, key, intValue);
}
else if(source.find("\"") != string::npos)
{
PyObject* strValue = PyStr_Create(source.substr(1, source.size()-2).c_str());
PyObject* key = PyStr_Create(target.c_str());
PyDict_SetItem(m_LocalEnvironment, key, strValue);
}
else if((pos = source.find("+")) != string::npos)
{
PyObject* leftObject = GetObjectBySymbol(source.substr(0, pos));
PyObject* rightObject = GetObjectBySymbol(source.substr(pos+1));
if(leftObject != NULL && right != NULL && leftObject->type == rightObject->type)
{
PyObject* resultValue = (leftObject->type)->add(leftObject, rightObject);
PyObject* key = PyStr_Create(target.c_str());
PyDict_SetItem(m_LocalEnvironment, key, resultValue);
}
(m_LocalEnvironment->type)->print(m_LocalEnvironment);
}
}
通过字符串搜索,如果命令中出现“=”,就是一个赋值或加法过程;如果命令中出现“print”,就是一个输出过程。进一步,在ExcuteAdd中,又进一步进行字符串搜索,以确定是否需要有一个额外的加法过程。根据这些解析的结果进行不同的动作,就是Small Python中的解释过程。这个过程在CPython中是通过正常的编译过程来实现的,而且最后会得到字节码的编译结果。
在这里需要重点指出的是那个m_LocalEnvironment,这是一个PyDictObject对象,其中维护着Small Python运行过程中,动态创建的变量的变量名和变量值的映射。这个就是Small Python中的执行环境,Small Python正是靠它来维护运行过程中的所有变量的状态。在CPython中,运行环境实际上也是这样一个机制。当需要访问变量时,就从这个PyDictObject对象中查找变量的值。这一点在执行输出操作时可以看得很清楚:
PyObject*GetObjectBySymbol(string&symbol)
{
PyObject* key = PyStr_Create(symbol.c_str());
PyObject* value = PyDict_GetItem(m_LocalEnvironment, key);
if(value == NULL)
{
cout << "[Error] : " << symbol << " is not defined!!" << endl;
return NULL;
}
return value;
}
void ExcutePrint(string symbol)
{
PyObject* object = GetObjectFromSymbol(symbol);
if(object != NULL)
{
PyTypeObject* type = object->type;
type->print(object);
}
}
在这里,通过变量名symbol,获得了变量值object。而在刚才的ExcueteAdd中,我们将变量名和变量值建立了联系,并存放到m_LocalEnvironment中。这种一进一出的机制正是CPython执行时的关键,在以后对Python字节码解释器的详细剖析中,我们将真实而具体地看到这种机制。
好了,我们的Small Python几乎已经完成了,最后所缺的就是一个交互式环境:
char* info = "********** Python Research **********\n";
char* prompt = ">>> ";
void Excute()
{
cout << info;
cout << prompt;
while(getline(cin, m_Command){
if(m_Command.size() == 0){
cout << prompt;
continue;
}
else if(m_Command == "exit"){
return;
}
else{
ExcuteCommand(m_Command);
}
cout << prompt;
}
}
有了它,我们的Small Python就大功告成了。现在,来看一看我们的成果吧:
到这里,我们就结束了对Python中对象的探索,通过Small Python这一个简陋的承前启后的东西,我们将敲开Python运行时的大门:)那里是字节码,解释器,条件判断语句,函数,类,异常等等的神秘世界,提起精神,我们出发了……
Python源码剖析
——字典对象PyDictObject(3)
本文作者: Robert Chen ([email protected])
4 PyDictObject对象缓冲池
前面我们提到,在PyDictObject的实现机制中,同样使用了缓冲池的技术:
[dictobject.c]
#define MAXFREEDICTS 80
static PyDictObject *free_dicts[MAXFREEDICTS];
static int num_free_dicts = 0;
实际上PyDictObject中使用的这个缓冲池机制与PyListObject中使用的缓冲池机制是一样的。开始时,这个缓冲池里什么都没有,直到有第一个PyDictObject被销毁时,这个缓冲池才开始接纳被缓冲的PyDictObject对象:
[dictobject.c]
static void dict_dealloc(register dictobject *mp)
{
register dictentry *ep;
int fill = mp->ma_fill;
PyObject_GC_UnTrack(mp);
Py_TRASHCAN_SAFE_BEGIN(mp)
//调整dict中对象的引用计数
for (ep = mp->ma_table; fill > 0; ep++) {
if (ep->me_key) {
–fill;
Py_DECREF(ep->me_key);
Py_XDECREF(ep->me_value);
}
}
//向系统归还从堆上申请的空间
if (mp->ma_table != mp->ma_smalltable)
PyMem_DEL(mp->ma_table);
//将被销毁的PyDictObject对象放入缓冲池
if (num_free_dicts < MAXFREEDICTS && mp->ob_type == &PyDict_Type)
free_dicts[num_free_dicts++] = mp;
else
mp->ob_type->tp_free((PyObject *)mp);
Py_TRASHCAN_SAFE_END(mp)
}
和PyListObject中缓冲池的机制一样,缓冲池中只保留了PyDictObject对象,而PyDictObject对象中维护的从堆上申请的table的空间则被销毁,并归还给系统了。具体原因参见PyListObject的讨论。而如果被销毁的PyDictObject中的table实际上并没有从系统堆中申请,而是指向PyDictObject固有的ma_smalltable,那么只需要调整ma_smalltable中的对象引用计数就可以了。
在创建新的PyDictObject对象时,如果在缓冲池中有可以使用的对象,则直接从缓冲池中取出使用,而不需要再重新创建:
[dictobject.c]
PyObject* PyDict_New(void)
{
register dictobject *mp;
…………
if (num_free_dicts) {
mp = free_dicts[--num_free_dicts];
_Py_NewReference((PyObject *)mp);
if (mp->ma_fill) {
EMPTY_TO_MINSIZE(mp);
}
}
…………
}
现在我们可以根据对PyDictObject的了解,在Python源代码中添加代码,动态而真实地观察Python运行时PyDictObject的一举一动了。
我们首先来观察,在insertdict发生之后,PyDictObject对象中table的变化情况。由于Python内部大量地使用PyDictObject,所以对insertdict的调用会非常频繁,成千上万的PyDictObject对象会排着长队来依次使用insertdict。如果只是简单地输出,我们立刻就会被淹没在输出信息中。所以我们需要一套机制来确保当insertdict发生在某一特定的PyDictObject对象身上时,才会输出信息。这个PyDictObject对象当然是我们自己创建的对象,必须使它有区别于Python内部使用的PyDictObject对象的特征。这个特征,在这里,我把它定义为PyDictObject包含“Python_Robert”的PyStringObject对象,当然,你也可以选用自己的特征串。如果在PyDictObject中找到了这个对象,则输出信息。
static void ShowDictObject(dictobject* dictObject)
{
dictentry* entry = dictObject->ma_table;
int count = dictObject->ma_mask+1;
int i;
for(i = 0; i < count; ++i)
{
PyObject* key = entry->me_key;
PyObject* value = entry->me_value;
if(key == NULL)
{
printf("NULL");
}
else
{
(key->ob_type)->tp_print(key, stdout, 0);
}
printf("\t");
if(value == NULL)
{
printf("NULL");
}
else
{
(key->ob_type)->tp_print(value, stdout, 0);
}
printf("\n");
++entry;
}
}
static void
insertdict(register dictobject *mp, PyObject *key, long hash, PyObject *value)
{
……
{
dictentry *p;
long strHash;
PyObject* str = PyString_FromString("Python_Robert");
strHash = PyObject_Hash(str);
p = mp->ma_lookup(mp, str, strHash);
if(p->me_value != NULL && (key->ob_type)->tp_name[0] == ‘i’)
{
PyIntObject* intObject = (PyIntObject*)key;
printf("insert %d\n", intObject->ob_ival);
ShowDictObject(mp);
}
}
}
对于PyDictObject对象,依次插入9和17,根据PyDictObject选用的hash策略,这两个数会产生冲突,9的hash结果为1,而17经过再次探测后,会获得hash结果为7。图7是观察结果:
然后将9删除,则原来9的位置会出现一个dummy态的标识。然后将17删除,并再次插入17,显然,17应该出现在原来9的位置,而原来17的位置则是dummy标识。图8是观察结果。
下面我们观察Python内部对PyDictObject的使用情况,在dict_dealloc中添加代码监控Python在执行时调用dict_dealloc的频度,图9是监测结果。
我们前面已经说了,Python内部大量使用了PyDictObject对象,然而监测的结果还是让我们惊讶不已,原来对于一个简简单单的赋值,一个简简单单的打印,Python内部都会创建并销毁多达8个的PyDictObject对象。不过这其中应该有参与编译的PyDictObject对象,所以在执行一个完整的Python源文件时,并不是每一行都会有这样的八仙过海:)当然,我们可以看到,这些PyDictObject对象中entry的个数都很少,所以只需要使用ma_smalltable就可以了。这里,也指出了PyDictObject缓冲池的重要性。
所以我们也监控了缓冲池的使用,在dict_print中添加代码,打印当前的num_free_dicts值。监控结果见图10。有一点奇怪的是,在创建了d2和d3之后,num_free_dicts的值仍然都是8。直觉上来讲,它们对应的是应该是6和5才对。但是,但是,:),看一看左边的图9,其实在执行print语句的时候,同样会调用dealloc8次,所以每次打印出来,num_free_dicts的值都是8。在后来del d2和del d1时,每次除了Python例行的8大对象的销毁,还有我们自己创建的对象的销毁,所以打印出来的num_free_dicts的值是9和10。