搬家喽,新家在http://blog.csdn.net/balabalamerobert
沉寂了这么久,希望以后能多多更新
搬家喽,新家在http://blog.csdn.net/balabalamerobert
沉寂了这么久,希望以后能多多更新
[绝对原创,转载请注明出处]
Python源码剖析
——Python执行引擎之一般表达式(2)
本文作者: Robert Chen(search.pythoner@gmail.com )
前面我们看了创建空的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(search.pythoner@gmail.com )
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(search.pythoner@gmail.com )
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 (search.pythoner@gmail.com )
通常认为,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)
{
WFILE wf;
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 (search.pythoner@gmail.com )
在详细考察了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…结构:
void ExcuteCommand(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);
}
}
void ExcuteAdd(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 (search.pythoner@gmail.com)
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。
[绝对原创 转载请注明出处]
Python源码剖析
——字典对象PyDictObject(2)
本文作者: Robert Chen (search.pythoner@gmail.com)
[dictobject.c]
typedef PyDictEntry dictentry;
typedef PyDictObject dictobject;
#define INIT_NONZERO_DICT_SLOTS(mp) do { \
(mp)->ma_table = (mp)->ma_smalltable; \
(mp)->ma_mask = PyDict_MINSIZE - 1; \
} while(0)
#define EMPTY_TO_MINSIZE(mp) do { \
memset((mp)->ma_smalltable, 0, sizeof((mp)->ma_smalltable)); \
(mp)->ma_used = (mp)->ma_fill = 0; \
INIT_NONZERO_DICT_SLOTS(mp); \
} while(0)
PyObject* PyDict_New(void)
{
register dictobject *mp;
if (dummy == NULL) { /* Auto-initialize dummy */
dummy = PyString_FromString("<dummy key>");
if (dummy == NULL)
return NULL;
}
if (num_free_dicts)
{
…… //使用缓冲池
}
else
{
mp = PyObject_GC_New(dictobject, &PyDict_Type);
if (mp == NULL)
return NULL;
EMPTY_TO_MINSIZE(mp);
}
mp->ma_lookup = lookdict_string;
_PyObject_GC_TRACK(mp);
return (PyObject *)mp;
}
值得注意的是,在第一次调用PyDict_New时,会创建在前面提到的那个dummy对象。显而易见,dummy对象仅仅是一个PyStringObject对象,它作为一种指示标志,表明该entry曾被使用过,且探测序列下一个位置的entry有可能是有效的,从而防止探测序列中断。
从num_free_dicts可以看出,Python中dict的实现同样使用了缓冲池。我们把将缓冲池的讨论放到后边。
创建的过程首先申请合适的内存空间,然后在EMPTY_TO_MINSIZE中,会将ma_smalltable清零,同时设置ma_size和ma_fill,当然,在一个PyDictObject对象刚被创建的时候,这两个变量都应该是0。然后会将ma_table指向ma_smalltable,并设置ma_mask,可以看到,ma_mask确实与一个PyDictObject对象中entry的数量有关。在创建过程的最后,将lookdict_string赋给了ma_lookup。正是ma_lookup指定了PyDictObject在entry集合中搜索某一特定entry时要进行的动作,它是PyDictObject的搜索策略,万众瞩目。
PyDictObject引入了两个不同的搜索策略,lookdict和lookdict_string。实际上,这两个策略使用的是相同的算法,lookdict_string只是lookdict的一种针对PyStringObject对象的特化形式。以PyStringObject对象作为PyDictObject对象中entry的键在Python中是如此地广泛和重要,所以lookdict_string也就成为了PyDictObject创建时所默认采用的搜索策略:
[dictobject.c]
static dictentry* lookdict_string(dictobject *mp, PyObject *key, register long hash)
{
register int i;
register unsigned int perturb;
register dictentry *freeslot;
register unsigned int mask = mp->ma_mask;
dictentry *ep0 = mp->ma_table;
register dictentry *ep;
if (!PyString_CheckExact(key)) {
mp->ma_lookup = lookdict;
return lookdict(mp, key, hash);
}
//[1]
i = hash & mask;
ep = &ep0[i];
//[2]
//if NULL or interned
if (ep->me_key == NULL || ep->me_key == key)
return ep;
//[3]
if (ep->me_key == dummy)
freeslot = ep;
else
{
//[4]
if (ep->me_hash == hash && _PyString_Eq(ep->me_key, key))
{
return ep;
}
freeslot = NULL;
}
/* In the loop, me_key == dummy is by far (factor of 100s) the
least likely outcome, so test for that last. */
for (perturb = hash; ; perturb >>= PERTURB_SHIFT)
{
i = (i << 2) + i + perturb + 1;
ep = &ep0[i & mask];
if (ep->me_key == NULL)
return freeslot == NULL ? ep : freeslot;
if (ep->me_key == key
|| (ep->me_hash == hash
&& ep->me_key != dummy
&& _PyString_Eq(ep->me_key, key)))
return ep;
if (ep->me_key == dummy && freeslot == NULL)
freeslot = ep;
}
}
其中的[1],[2],[3],[4]标注出了搜索过程中的关键步骤,这些步骤会在后面讲述PyDictObject对象的一般搜索策略时详细讨论。
lookdict_string并不是PyDictObject中最一般的搜索策略,它是一种有条件限制的搜索策略。lookdict_string背后有一个假设,即PyDictObject对象中每一个entry的key都是PyStringObject*。只有在这种假设成立的情况下,lookdict_string才会被使用。可以看到,lookdict_string首先会检查需要搜索的key是否严格对应一个PyStringObject对象,只有在检查通过后,才会进行下面的动作;如果检查不通过,那么就会转向PyDictObject中的通用搜索策略lookdict:
[dictobject.c]
static dictentry* lookdict(dictobject *mp, PyObject *key, register long hash)
{
register int i;
register unsigned int perturb;
register dictentry *freeslot;
register unsigned int mask = mp->ma_mask;
dictentry *ep0 = mp->ma_table;
register dictentry *ep;
register int restore_error;
register int checked_error;
register int cmp;
PyObject *err_type, *err_value, *err_tb;
PyObject *startkey;
//[1]
i = hash & mask;
ep = &ep0[i];
//[2]
if (ep->me_key == NULL || ep->me_key == key)
return ep;
//[3]
if (ep->me_key == dummy)
freeslot = ep;
else
{
//[4]
if (ep->me_hash == hash)
{
startkey = ep->me_key;
cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
if (cmp < 0)
PyErr_Clear();
if (ep0 == mp->ma_table && ep->me_key == startkey)
{
if (cmp > 0)
goto Done;
}
else
{
ep = lookdict(mp, key, hash);
goto Done;
}
}
freeslot = NULL;
}
。。。。。。
Done:
return ep;
}
PyDictObject中维护的entry的数量是有限的,比如100个或1000个。而传入lookdict中的key的hash值却不一定会在这个范围内,所以这就要求lookdict将hash值映射到某个entry上去。Lookdict采取的策略非常简单,直接将hash值与entry的数量做一个与操作,结果自然落在entry的数量之下。由于ma_mask会被用来进行大量的与操作,所以这个与entry数量相关的变量被命名为ma_mask,而不是ma_size。
我们注意到,lookdict永远都不会返回NULL,如果在PyDictObject中搜索不到待查找的元素,同样会返回一个entry,这个entry的me_value为NULL。
在搜索的过程中freeslot是一个重要的变量。如果在探测序列中的某个位置上,entry处于Dummy态,那么如果在这个序列中搜索不成功,就会返回这个处于Dummy态的entry。我们知道,处于Dummy态的entry其me_value是为NULL的,所以这个返回结果指示了搜索失败;同时,返回的entry也是一个可以立即被使用的entry,因为Dummy态的entry并没有维护一个有效的(key,value)对。这个freeslot正是用来指向探测序列中第一个处于Dummy态的entry,如果搜索失败,freeslot就会挺身而出,提供一个能指示失败并立即可用的entry。当然,如果探测序列中并没有Dummy态entry,搜索失败时,一定是在一个处于Unused态的entry上结束搜索过程的,这时会返回这个处于Unused态的entry,同样是一个能指示失败且立即可用的entry。
下面是lookdict中进行第一次检查时需要注意的动作:
[1]:根据hash值获得entry的序号。
[2]:如果ep->me_key为NULL,且与key相同,搜索失败。
[3]:若当前entry处于Dummy态,设置freeslot。
[4]:检查当前Active的entry中的key与待查找的key是否相同,如果相同,则立即返回,搜索成功。
在[4]中,需要注意那个PyObject_RichCompareBool,它的函数原形为:
int PyObject_RichCompareBool(PyObject *v, PyObject *w, int op)
当(v op w)成立时,返回1;当(v op w)不成立时,返回0;如果在比较中发生错误,则返回-1。
现在我们考察的是根据hash值获得的第一个entry与待查找的元素的比较。实际上,由于对应于某一个散列值,几乎都有一个探测序列与之对应,所以我们现在只是考察了探测序列中第一个位置的entry。万里长征仅仅迈出了第一步。
如果第一个entry与待查找的key不匹配,那么很自然地,lookdict会沿着探测序列,顺藤摸瓜,依次比较探测序列上的entry与待查找的key:
[dictobject.c]
static dictentry* lookdict(dictobject *mp, PyObject *key, register long hash)
{
register int i;
register unsigned int perturb;
register dictentry *freeslot;
register unsigned int mask = mp->ma_mask;
dictentry *ep0 = mp->ma_table;
register dictentry *ep;
register int restore_error;
register int checked_error;
register int cmp;
PyObject *err_type, *err_value, *err_tb;
PyObject *startkey;
。。。。。。
for (perturb = hash; ; perturb >>= PERTURB_SHIFT)
{
//[5]
i = (i << 2) + i + perturb + 1;
ep = &ep0[i & mask];
//[6]
if (ep->me_key == NULL)
{
if (freeslot != NULL)
ep = freeslot;
break;
}
if (ep->me_key == key)//[7]
break;
if (ep->me_hash == hash && ep->me_key != dummy)
{
startkey = ep->me_key;
cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
if (cmp < 0)
PyErr_Clear();
if (ep0 == mp->ma_table && ep->me_key == startkey) {
if (cmp > 0)
break;
}
else {
ep = lookdict(mp, key, hash);
break;
}
}
//[8]
else if (ep->me_key == dummy && freeslot == NULL)
freeslot = ep;
}
Done:
return ep;
}
[5]:获得探测序列中的下一个待探测的entry。
[6]:ep到达一个Unused态entry,表明搜索结束。这是如果freeslot不为空,则返回freeslot所指entry。
[7]:entry与待查找的key匹配,搜索成功。
[8]:在探测序列中发现Dummy态entry,设置freeslot。
到这里,我们已经清晰地了解了PyDictObject中的搜索策略。再回过头去看看那个lookdict_string,可以很清晰地看到,lookdict_string实际上就是一个lookdict对于PyStringDict对象的优化版本。在这里展示的lookdict代码经过了删节,实际上,在lookdict中有许多捕捉错误并处理错误的代码,因为lookdict面对的是PyObject*,所以会出现很多意外情况。而在lookdict_string中,完全没有了这些处理错误的代码。而另一方面,在lookdict中,使用的是非常通用的PyObject_RichCompareBool,而lookdict_string使用的是_PyString_Eq,比要简单很多,这些因素使得lookdict_string的搜索效率要比lookdict高很多。
此外,Python自身也大量使用了PyDictObject对象,用来维护一个作用域中变量名和变量值之间的对应关系,或是用来在为函数传递参数时维护参数名与参数值的对应关系。这些对象几乎都是用PyStringObject对象作为entry中的key,所以lookdict_string的意义就显得非常重要了,它对Python整体的运行效率都有着重要的影响。
PyDictObject对象中元素的插入动作建立在搜索的基础之上:
[dictobject.c]
static void
insertdict(register dictobject *mp, PyObject *key, long hash, PyObject *value)
{
PyObject *old_value;
register dictentry *ep;
ep = mp->ma_lookup(mp, key, hash);
//[1]
if (ep->me_value != NULL) {
old_value = ep->me_value;
ep->me_value = value;
Py_DECREF(old_value); /* which **CAN** re-enter */
Py_DECREF(key);
}
//[2]
else {
if (ep->me_key == NULL)
mp->ma_fill++;
else
Py_DECREF(ep->me_key);
ep->me_key = key;
ep->me_hash = hash;
ep->me_value = value;
mp->ma_used++;
}
}
前面我们提到了,搜索操作在成功时,返回相应的处于Active态的entry,而在搜索失败时会返回两种不同的结果:一是处于Unused态的entry;二是处于Dummy态的entry。那么插入操作对应不同的entry,所需要进行的动作显然也是不一样的。对于Active的entry,只需要简单地替换me_value值就可以了;而对于Unused或Dummy的entry,则需要完整地设置me_key,me_hash和me_value。
在insertdict中,正是根据搜索的结果采取了不同的动作:
[1] :搜索成功,返回处于Active的entry,直接替换me_value。
[2] :搜索失败,返回Unused或Dummy的entry,完整设置me_key,me_hash和me_value。
在Python中,对PyDictObject中的元素进行插入或设置有两种方式:
[python code]
d = {}
d[1] = 1
d[1] = 2
第二行Python代码是在PyDictObject对象中没有这个entry的情况下插入元素,第三行是在PyDictObject对象中已经有这个entry的情况下重新设置元素。可以看到,insertdict完全可以适应这两种情况,在insertdict中,[2]处代码处理第二行Python代码,[1]处代码处理第三行Python代码。实际上,这两行Python代码也确实都调用了insertdict。
当这两行设置PyDictObject对象元素的Python代码被执行时,并不是直接就调用insertdict,因为观察代码可以看到,insertdict需要一个hash值作为调用参数,那么这个hash值在什么地方获得的呢?实际上,在调用insertdict之前,还会调用PyDict_SetItem:
[dictobject.c]
int PyDict_SetItem(register PyObject *op, PyObject *key, PyObject *value)
{
register dictobject *mp;
register long hash;
register int n_used;
mp = (dictobject *)op;
//计算hash值
if (PyString_CheckExact(key)) {
hash = ((PyStringObject *)key)->ob_shash;
if (hash == -1)
hash = PyObject_Hash(key);
}
else {
hash = PyObject_Hash(key);
if (hash == -1)
return -1;
}
n_used = mp->ma_used;
Py_INCREF(value);
Py_INCREF(key);
insertdict(mp, key, hash, value);
if (!(mp->ma_used > n_used && mp->ma_fill*3 >= (mp->ma_mask+1)*2))
return 0;
return dictresize(mp, mp->ma_used*(mp->ma_used>50000 ? 2 : 4));
}
在PyDict_SetItem中,会首先获得key的hash值,在上面的例子中,也就是一个PyIntObject对象1的hash值。
然后再调用insertdict进行元素的插入或设置。
PyDict_SetItem在插入或设置元素的动作结束之后,并不会草草返回了事。接下来,它会检查是否需要改变PyDictObject内部ma_table所值的内存区域的大小,在以后的叙述中,我们将这块内存称为“table”。那么什么时候需要改变table的大小呢。在前面我们说过,如果table的装载率大于2/3时,后续的插入动作遭遇到冲突的可能性会非常大。所以装载率是否大于或等于2/3就是判断是否需要改变table大小的准则。在PyDict_SetItem中,有如下的代码:
if (!(mp->ma_used > n_used && mp->ma_fill*3 >= (mp->ma_mask+1)*2))
return 0;
经过转换,实际上可以得到:
(mp->ma_fill)/(mp->ma_mask+1) >= 2/3
这个等式左边的表达式正是装载率。
然而装载率只是判定是否需要改变table大小的一个标准,还有另一个标准是在insertdict的过程中,是否使用了一个处于Unused态的entry。前面我们说过,在搜索过程失败并且探测序列中没有Dummy态的entry时,就会返回一个Unused态的entry,insertdict会对这个entry进行填充。只有当这种情况发生并且装载率超标时,才会进行改变table大小的动作。而判断在insertdict的过程中是否填充了Unused态的entry,是通过
mp->ma_used > n_used
来判断的,其中的n_used就是进行insertdict操作之前的mp->ma_used。通过观察mp->ma_used是否改变,就可以知道是否有Unused态的entry被填充。
在改变table时,并不一定是增加table的大小,同样也可能是减小table的大小。更改table的大小时,新的table的空间为:
mp->ma_used*(mp->ma_used>50000 ? 2 : 4)
如果一个PyDictObject对象的table中只有几个entry处于Active态,而大多数entry都处于Dummy态,那么改变table大小的结果显然就是减小了table的空间大小。
在确定新的table的大小时,通常选用的策略是时新的table中entry的数量是现在table中Active态entry数量的4倍,选用4倍是为了使table中处于Active态的entry的分布更加稀疏,减少插入元素时的冲突概率。当然,这是以内存空间为代价的。由于机器的内存总是有限的,Python总不能没心没肺地在任何时候都要求4倍空间,这样搞,别的程序会有意见的:)所以当table中Active态的entry数量非常巨大时,Python只会要求2倍的空间,这次又是以执行速度来交换内存空间。Python2.4.1将这个“非常巨大”的标准划定在50000。如此一来,上帝的归上帝,撒旦的归撒旦,万事大吉 :)
至于具体的改变table大小的重任,则交到了dictresize一人的肩上:
[dictobject.c]
static int dictresize(dictobject *mp, int minused)
{
int newsize;
dictentry *oldtable, *newtable, *ep;
int i;
int is_oldtable_malloced;
dictentry small_copy[PyDict_MINSIZE];
//[1]
for(newsize = PyDict_MINSIZE; newsize <= minused && newsize > 0; newsize <<= 1)
;
oldtable = mp->ma_table;
assert(oldtable != NULL);
is_oldtable_malloced = oldtable != mp->ma_smalltable;
//[2]
if (newsize == PyDict_MINSIZE) {
newtable = mp->ma_smalltable;
if (newtable == oldtable) {
if (mp->ma_fill == mp->ma_used) {
//没有任何Dummy态entry,直接返回
return 0;
}
//将oldtable拷贝,进行备份
assert(mp->ma_fill > mp->ma_used);
memcpy(small_copy, oldtable, sizeof(small_copy));
oldtable = small_copy;
}
}
else {
newtable = PyMem_NEW(dictentry, newsize);
}
//[3]
assert(newtable != oldtable);
mp->ma_table = newtable;
mp->ma_mask = newsize - 1;
memset(newtable, 0, sizeof(dictentry) * newsize);
mp->ma_used = 0;
i = mp->ma_fill;
mp->ma_fill = 0;
//[4]
for (ep = oldtable; i > 0; ep++) {
if (ep->me_value != NULL) { /* active entry */
–i;
insertdict(mp, ep->me_key, ep->me_hash, ep->me_value);
}
else if (ep->me_key != NULL) { /* dummy entry */
–i;
assert(ep->me_key == dummy);
Py_DECREF(ep->me_key);
}
}
if (is_oldtable_malloced)
PyMem_DEL(oldtable);
return 0;
}
[1] :dictresize首先会确定新的table的大小,很显然,这个大小一定要大于传入的参数minused,这也是在原来的table中处于Active态的entry的数量。dictresize从8开始,以指数方式增加大小,直到超过了minused为止。所以实际上新的table的大小在大多数情况下至少是原来table中Active态entry数量的4倍。
[2] :如果在[1]中获得的新的table大小为8,则不需要在堆上分配空间,直接使用ma_smalltable就可以了;否则,则需要在堆上分配空间。
[3] :对新的table进行初始化,并调整原来PyDictObject对象中用于维护table使用情况的变量。
[4] :对原来table中的非Unused态entry进行处理。对于Active态entry,显然需要将其插入到新的table中,这个动作由前面考察过的insertdict完成;而对于Dummy态的entry,则略过,不做任何处理,因为我们知道Dummy态entry存在的唯一理由就是为了不使搜索时的探测序列中断。现在所有Active态的entry都重新依次插入新的table中,它们会形成一条新的探测序列,不再需要这些Dummy态的entry了。
现在,利用我们对PyDictObject的认识,想象一下从table中删除一个元素应该怎样操作呢?
[dictobject.c]
int PyDict_DelItem(PyObject *op, PyObject *key)
{
register dictobject *mp;
register long hash;
register dictentry *ep;
PyObject *old_value, *old_key;
//获得hash值
if (!PyString_CheckExact(key) ||
(hash = ((PyStringObject *) key)->ob_shash) == -1) {
hash = PyObject_Hash(key);
if (hash == -1)
return -1;
}
//搜索entry
mp = (dictobject *)op;
ep = (mp->ma_lookup)(mp, key, hash);
//删除entry所维护的元素
old_key = ep->me_key;
Py_INCREF(dummy);
ep->me_key = dummy;
old_value = ep->me_value;
ep->me_value = NULL;
mp->ma_used–;
Py_DECREF(old_value);
Py_DECREF(old_key);
return 0;
}
流程非常清晰,先计算hash值,然后搜索相应的entry,最后删除entry中维护的元素,并将entry从Active态变换为Dummy态,同时还将调整PyDictObject对象中维护table使用情况的变量。
下面我们用一个简单的例子来动态地展示对PyDictObject中table的维护过程,需要提醒的是,这里采用的散列函数和探测函数都与Python中PyDictObject实际采用的策略不同,这里只是想要从观念上展示对table的维护过程。在下面的图中,蓝色代表Unused态entry,绿色为Active态,黄色为Dummy态。
假如table中有10个entry,散列函数为HASH(x) = x mod 10,冲突解决方案采用线性探测,且探测函数为x = x + 1。假设向table中依次加入了以下元素对:(4,4),(14,14),(24,24),(34,34),则加入元素后的entry为:
现在删除元素对(14,14),然后向table中插入新的元素对(104,104)。则在搜索的过程中,由于在原来维护14的entry#4处于现在Dummy态,所以freeslots会指向这个可用的entry:
搜索完成后,填充freeslot所指向的entry:
然后,再向table中插入元素对(14,14),这时由于探测序列上已经没有Dummy态entry了,所以最后返回的ep会指向一个处于Unused态的entry:
最后插入元素对(14,14),结果为:
[绝对原创 转载请注明出处]
Python源码剖析
——字典对象PyDictObject(1)
本文作者: Robert Chen (search.pythoner@gmail.com)
元素和元素之间通常可能存在某种联系,这种神秘的联系使本来绝不相同的两个元素捆绑在一起,而别的元素则被排斥在外。比如对应于“2倍”,这样一种联系,6和3就是这样的两个元素,而4和2同样也是被这种联系关联起来的一对元素。
为了刻画某种对应关系,现代的编程语言通常都在语言级或标准库中提供某种关联式的容器。关联式的容器中存储着一对对符合该容器所代表的关联规则的元素对。关联式容器中的元素对通常是以(健key,值value)这样的形式存在。比如在一个表示“2倍”关系的关联容器中(3,6),(2,4)就是容器中的两个元素对。其中3就是一个“键”,当寻找到3之后,我们很轻松地就能获得与3有着“2倍”联系的另一元素。
关联容器的设计总会极大地关注搜索键的效率,因为通常我们使用关联容器,都是希望根据我们手中已有的某个元素来快速获得与之有某种联系的另一元素。一般而言,关联容器的实现都会基于设计良好的数据结构。比如C++的STL中的map就是一种关联容器,map的实现基于RB-tree(红黑树)。RB-tree是一种平衡二元树,能够提供良好的搜索效率,理论上,其搜索的复杂度为O(logN)。
Python中同样提供关联式容器,即PyDictObject对象。与map不同的是,PyDictObject对搜索的效率要求及其苛刻,这也是因为PyDictObject在Python本身的实现中被大量地采用,比如会通过PyDictObject来建立Python字节码的运行环境,其中会存放(变量名,变量值)的元素对,通过查找变量名获得变量值。因此,PyDictObject没有如map一样采用平衡二元树,而是采用了散列表(hash table),因为理论上,在最优情况下,散列表能提供O(1)复杂度的搜索效率。
散列表的基本思想是通过一定的函数将需搜索的键值映射为一个整数,将这个整数视为索引值去访问某片连续的内存区域。看一个简单的例子,如图1所示,有10个整数1,2,……,10。其依次对应a, b, ……, j。申请一块连续内存,并依次存储a, b, ……, j:
当需要寻找与2对应的字母时,只需通过一定的函数将其映射为整数,显然,2本身就是一个整数,我们可以使用2自身。然后访问这片连续内存的第2个位置,就能得到与2对应的字母b。
将元素映射为整数的过程对于散列表来说尤为关键,用于映射的函数成为散列函数(hash function),而映射后的值称为元素的散列值(hash value)。
在使用散列表的过程中,不同的对象经过散列函数的作用,可能被映射为相同的散列值。而且随着需要存储的数据的增多,这样的冲突就会发生得越来越频繁。散列冲突是散列技术与生俱来的问题。
有很多种方法可以用来解决产生的散列冲突,比如说开链法,这是SGI STL中的hash table所采用的方法;而Python中所采用的是另一种方法,即开放定址法。
当产生散列冲突时,Python会通过一个再次探测函数,计算下一个候选位置,如果这个位置可用,则可将待插入元素放到这个位置;如果这个位置不可用,则Python会再次通过探测函数,获得下一个候选位置,如此不断探测,总会找到一个可用的位置。
这样,沿着探测函数,从一个位置出发可以依次到达多个位置,这些位置形成了一个探测链。当需要删除某条探测链上的某个元素时,问题就产生了。假如这条链的首元素位置为a,尾元素的位置为c,现在需要删除中间的某个位置b上的元素。如果直接将位置b上的元素删除,则会导致探测链的断裂,引起严重的问题。想象一下,在下一次搜索c时,会从位置a开始,通过探测函数,沿着探测链,一步一步向位置c靠近,但是在到达位置b时,发现这个位置上的元素钚属于这个探测链,因此会以为探测链在这里结束,导致不能达到为止c,自然也不能搜索到位置c上的元素,所以结果是搜索失败。而实际上,待搜索的元素确实存在于散列表中。
所以,在采用开放定址的冲突解决策略的散列表中,删除某条探测链上的元素时不能真正地删除,而是进行一种“伪删除”操作,必须要让该元素还存在于探测链上,担当承前启后的重任。对于这种伪删除技术,我们在分析Python中的关联容器时会详细讨论。
在此后的描述中,我们将把关联容器中的一个(键,值)元素对称为一个entry或slot。在Python中,一个entry的定义如下:
[dictobject.h]
typedef struct {
long me_hash; /* cached hash code of me_key */
PyObject *me_key;
PyObject *me_value;
} PyDictEntry;
可以看到,在PyDictObject中其实存放的都是PyObject*,这也是Python中的dict什么都能装得下的原因,因为在Python中,不论什么都是一个PyObject对象。
me_hash域中存储的是me_key的散列值,利用一个域来记录这个散列值可以避免每次查询的时候都要重新计算一遍散列值。
在Python中,在一个PyDictObject对象生存变化的过程中,其中的entry还会在不同的状态间转换。PyDictObject中entry可以在三种状态间转换:Unused态,Active态,Dummy态。
1.当一个entry的me_key和me_value都是NULL时,entry处于Unused态。表明该entry中并没有存储(key, value)对,而且在此之前,也没有存储过。每一个entry在初始化的时候都会出于这种状态,而且只有在Unused态下,entry的me_key域才会为NULL。
2.当entry中存储了一个(key,value)对时,entry便转换到了Active态。在Active态下,me_key和me_value都不能为NULL。更进一步,me_key不能是dummy。
3.当entry中存储的(key,value)对被删除后,entry中的me_key将指向dummy,entry进入Dummy态。这是一种惰性删除技巧,是为了保证冲突的探测序列不会在这里被中断。
图2展示了三种状态以及它们之间的转换关系:
一个PyDictObject对象实际上是一大堆entry的集合,总控这些集合的结构如下:
[dictobject.h]
#define PyDict_MINSIZE 8
typedef struct _dictobject PyDictObject;
struct _dictobject {
PyObject_HEAD
int ma_fill; /* # Active + # Dummy */
int ma_used; /* # Active */
int ma_mask;
PyDictEntry *ma_table;
PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
PyDictEntry ma_smalltable[PyDict_MINSIZE];
};
从注释中可以清楚地看到,ma_fill域中维护着从PyDictObject创建到现在曾经以及正处于Active态的entry个数,而ma_used则维护着当前正处于Active态的entry。
当创建一个PyDictObjec对象时,至少有PyDict_MINSIZE个entry被同时创建。在dictobject.h中,这个值被设定为8,这个值被认为是通过大量的实验得出的最佳值。既不会太浪费内存空间,又能很好地满足Python中大量使用PyDictObject的环境的需求,而不需要再次调用malloc申请内存空间。当然,我们可以自己调节这个值来调节Python的运行效率。
当一个PyDictObject是一个比较小的dict时,即entry数量少于8个,ma_table域将指向这与生俱来的8个entry。而当PyDictObject中的entry数量超过8个时,Python认为这家伙是一个大dict了,将会额外申请内存空间,并将ma_table指向这块空间。这样,无论何时,ma_table域总不会为NULL,这带来了一个好处,不用再运行时一次又一次不厌其烦地检查ma_table的有效性,因为ma_table总是有效的。
PyDictObject中的ma_mask实际上记录了一个PyDictObject对象中有entry的数量。至于这家伙为什么不叫ma_size这么好听的名字,偏偏要叫ma_mask这个莫名其妙的名字,此乃后话,这里先按下不表。同样被按下不表的还有ma_lookup :)