python源码分析:string对象的实现

通过在分析源码与测试分析预期时,我们发现这样一个现象,就是python的这些基础数据类型(intstringlistdict)其实在实现python时(注意,是实现python自身时)就开始运用了。比如后面说到的stringintern机制就使用了dict来形成映射。好了,言归正传,下面开始分析任何语言中最为关键的数据类型:String类型。

前面知道了,任何对象都是从PyObject“继承”过来的,其实就是把PyObject_HEAD或更进一层的的PyObject_VAR_HEAD放到了自己的object的结构体中,看见没,继承在更低层的语言看起来就是这么回事,只是在高层语言屏蔽掉了这些更浅显的逻辑再出现一些很抽象的概念(当然抽象有抽象的好处,容易与现实环境作融合)。string的所有的信息就在了这个PyStringObject结构体中,但注意可不是直接作为直接变量放到了PyStringObject,这还要追溯到PyObject中的PyObject_HEAD中的ob_type,算了,不追根溯源了。我是想说,string的所有的操作与数据都可以通过这个PyStringObject找到。

下面就让我分析一下pythonString实现比较出彩的两个地方:短字符串与长字符串的实现,及用时计算(主要是指stringhash值的计算)。

第一个,python的设计者的设计目的并不是为了提高效率,而是为了提高内存利用率。

为什么?因为通过阅读源码发现,即便python使用了intern(什么是intern,别急,后面会有介绍)机制,它还是要像没有这套机制一样生成一个PyStringObject。所以要记住,这个intern机制,是为了节约内存,不是为了提高执行效率。

什么是intern机制呢?先从小字符串及空字符串说起。与前面的小数字池的思路是一致的:你string尽管是变长对象int为固定长度对象),但你是不可变对象。即你一旦生成,就无法改变了。嗯!那我不是也可以重复利用已经生成的string,当然前提是我要重复利用的string的字符串内容和我要生成的字符串完全一致。但又不能过去激动,想想为什么python只实现了小整数池!所以我们不能把100个可打印字符的所有可重组合都缓存起来,那将只有sb才会做出来。我们就缓冲255个可打印字符,即单个字符,外加一个空字符。这是字符池思想,还不是intern机制的完整描述。下面我们看看大串的intern机制。

如果我们在c语言中,构造出两个相同的字符串,除非使用指针来引用,那么必须开辟两个内存空间。但在python语言中特定的环境下(string一旦生成不可更改),我们何必开辟两个呢,共用一个空间不就行了。这就是intern机制的思路。每次申请生成一个字符串时,都会去检查interned字典中有没这个字符串(还记得python中的字符串hash值吗?)如果有,那么返回字典中的字符串对象的引用(记得引用计数加1哦)。其实上面的单字符池也是在这个intern机制下的,即每次一旦生成一个单字符,先将单字符放入interned字典中,再将其放入全局的一个一维表中,以后每次取这个字符根据字符的ascii码值直接索引那个一维表。

这里不仅节约的内存,更提升的效率。

再说说用时计算的思想,其实这些思想和写时复制,延迟写都是一样的,即尽量延迟一些操作(因为这些操作在一些情况下是不需要进行的)来节约CPUI/O的开销。由hash值的计算是cpu密集型的,因此这个操作在大多数据字符串并不需要其hash值的情况下,延迟到其真正需要的时间点执行是必要的。(但这种思想可能在实时操作系统中并不受欢迎)。

再说说,如何在理解了python的源码的基础上,加强对python的合理使用。通过string对象的学习我们知道了string内部的实现,像s1+s2+s3就是调用了string_concat来实现了,由于string为不可变对象,通过阅读源码我们发现了,其实现就是简单地依次生成新对象。s1+s2+s3:初始状态

sn+s3s1s2生成了新的对象sn

smsns3生成了最终的对象sm

这幕后就在最多4次最少2次的对象生成操作(为什么是最多4次呢,若为长字符,intern机制之前都要生成一个PyStringObject),相应次数的销毁操作。

但如果使用stringjoin操作,我们看到string_join中的实现是:计数出要join的所有字符串的长度,然后进行一一拷贝,这样只有一次对象生成。

发表评论