python源码剖析 2-6章

来源:互联网 发布:react native java 编辑:程序博客网 时间:2024/06/07 09:43

第二章 Python中的整数对象

python中所有的内建对象,几乎都会有自己的对象池机制,以避免常用对象频繁的malloc和free

python中的int其实就是C中的long

PyIntObject对象的创建:

python为创建一个PyIntObject提供了三个方法PyInt_FromLong,PyInt_FromString,PyInt_FromUnicode实际上,PyInt_FromString,PyInt_FromUnicode是先把String或者Unicode转换成long或者float,然后调用PyInt_FromLong或者PyInt_FromFloat

小整数对象

intobject.c中small_ints定义了小整数对象的范围,小整数会在python启动的时候维护到block_list中,直到python关闭的时候才销毁

大整数对象-时间与空间的两难选择

我们不能保证大整数就不会被经常使用,也不能将大整数都缓存起来,那样太浪费空间,python该怎么做?

结构PyIntBlock中维护了一块内存(block),内存中保存了一定数量的PyIntObject对象(一般是82个,可以调整)。单向列表block_list维护PyIntBlock,每一个block都维护了一个PyIntObject数组--objects,数组中存储着真正的PyIntObject对象。  全部block的objects中的所有空闲内存由一个单向链表来维护,这个链表的表头是free_list当需要返回一个大整数对象时,就需要在block的objects中寻找一块可用于存储新的PyIntObject对象的内存,这个重任是free_list完成。首次调用PyInt_FromLong时,free_list必定为NULL,这时python会调用fill_free_list创建新的block,从而创建新的空闲内存,需要注意的是,每当空闲内存耗尽的时候,free_list都会指向NULL,从而在下一次PyInt_FromLong的时候,调用fill_free_list当对象被销毁时(即引用计数变为0时),python会把这个对象所在的内存地址维护到free_list这个单向链表中,以供其他对象使用需要注意的一点是,python销毁一个整数对象,是把这个对象所在的内存维护到free_list中去,并不是还给系统,即一旦系统堆中的某块内存被申请用于整数对象,那么这块内存在python结束之前是不会被释放的,这一漏洞有可能将系统内存全部吃光

第三章 Python中的字符串对象

  • 可变对象与不可变对象:可变对象维护的数据在创建之后还能再变化,比如list。不可变对象所维护的数据在对象创建之后就不能变化了,比如str和int。我们平时做的多个str连接操作,其实是先把原来的str对象free,然后再创建一个新的str对象。
  • 定长对象与变长对象:定长对象维护数据的长度在对象定义的时候就知道了,比如PyIntObject的长度就是C中long的长度。变长对象维护数据的长度要到对象创建的时候才知道。

本章要研究的字符串属于变长对象中的不可变对象。字符串作为不可变对象使得它可以作为dict的键值,但也使得一些字符串操作,比如多个字符串连接,的效率大大降低了。

python中,PyStringObject是对字符串对象的实现,PyStringObject中ob_shash变量的作用是缓存该对象的hash值,如果该对象还没有计算过hash值,那么ob_shash的初始值是-1。

  • 字符串对象的intert机制
intert机制的目的是:对被intert之后的字符串,在整个python运行期间,只有一个PyStringObject对象与之对应(即对于相同的字符串,不再单独的分配空间),这样既节省了空间,又简化了字符串比较intert机制的核心在于interned,interned实际上指向一个dict的集合,这个集合中记录着已经被intern的字符串对象,当新建一个PyStringObject对象时,会去检查dict集合中有没有相同的对象,如果有的话,将指向新建对象的指针指向dict集合,而新建对象的引用计数-1
  • 对于一个字符的字符串,python有着字符串缓冲池(对于其他字符串的处理,python是要先创建一个字符串对象,然后处理intern,然后销毁这个字符串对象。而对于一个字符的字符串,python直接使用缓冲池里的对象而不需要重新创建)

  • str的连接问题

python中通过+号连接字符串效率是极其低下的,根源在于PyStringObject是一个不可变对象,当进行字符串连接时,就必须创建一个新的对象,如果要连接N个字符串,就要进行N-1次内存申请及搬运工作推荐使用join对存储在list或tuple中的字符串进行连接,这种做法只需要进行一次的内存分配

第四章 python中的list对象

  • 在python的list中,无一例外存放的都是PyObject*指针
  • list的内存管理:并不是插入一个元素就申请一块内存,太低效了。而是一次申请一大块内存,内存的总大小由allocated维护,使用大小由ob_size维护

PyListObject对象的创建

1. PyList_New创建PyListObject对象并指定该列表初始元素个数(检查缓冲池(free_lists)中是否有可用对象,若有,直接用)  2. 检查元素实际占用的空间是否过大(第一步只是指定了元素个数,并没有计算元素实际占用空间)3. 若缓冲池没有可用对象,则申请内存4. 在申请的内存中创建PyListObjects*列表,列表里的每一项元素会被初始化为NULL5. 开始维护ob_size和allocated

插入操作

1. 类型检查,内存是否足够等2. 当newsize < allcated/2 时,python会收缩列表内存空间3. 在确定插入位置之后,python会将插入点之后的所有元素后移一个位置,这样就能在插入点空出一个位置了(如果是链表的话,只需要修改指针就可以了,不需要移动元素,所以猜测python的list是数组结构)

删除操作:python的list中replace和remove调用的是一个方法,当传入的参数是NULL时,就是删除,否则是替换

PyListObject对象缓冲池

1. 创建一个新的list时,其实是分为两步:先创建PyListObject对象,再创建这个对象维护的元素列表。销毁过程也是一样分为两步:先销毁元素列表,再销毁PyListObject对象  2. 在销毁PyListObject对象本身时,会检查free_lists缓冲池是否满了,如果没满,就会把这个对象加入到缓冲池当中。注意加入缓冲池的只是PyListObject对象本身,并不包括它维护的元素列表

第五章 Python中的dict对象

  • dict采用了散列表(hash table),最优情况的复杂度是O(1)
  • 散列表:通过一定的函数将一个键值映射为一个整数,然后将这个整数视为索引值去访问某片连续的内存区域
  • 散列冲突:不同对象经过散列函数的作用,有可能被映射为相同的散列值
  • 装载率:如果散列表可以容纳10个元素,已经容纳了6个,那么装载率就是6/10。当装载率大于2/3时,散列冲突就会越来越频繁
  • python中解决散列冲突使用开放定址法:当产生散列冲突时,通过使用二次探测函数f,计算下一个候选位置,如果还不可用,再次使用f。这样产生的链条叫做冲突探测链
  • 一个键值对称为一个entry:PyDictObject的entry会在三种状态间切换
  1. Unused:entry的me_key和me_value都是NULL且这个entry之前也没有存储过任何值,即初始化状态
  2. Active:entry中存储了键值对时,进入这个状态
  3. Dummy:entry中存储的键值对被删除后,转为dummy状态。不能转为Unused状态,这会导致冲突探测链的中断
  • PyDictObject默认初始创建8个entry。当初始插入的entry数量大于8时,会指向一片新申请的内存空间
  • PyDictObject中的元素搜索
1. python提供了两种dict搜索策略:lookdict和lookdict_string。lookdict_string是lookdict的特殊形式,也是默认的搜索策略,因为用string作为key是主流2. 当搜索成功时,返回entry;当搜索不成功时,返回的是一个能提示失败并可用的entry3.key搜索,key相同包含两层含义:引用相同和值相同。检查key相同时,先检查hash值是否相同(hash值不同则其值一定不同),再检查引用是否相同,再检查值是否相同4. 当待搜索的key是PyStringObject对象时,就会调用lookdict_string
  • 插入:插入键值对后,如果装载率大于2/3了,那么会重新申请内存空间,将数据迁移过去,迁移的过程中,无用的dummy态entry会被丢弃,迁移完成后,原内存空间free。
  • 在删除,dummy过多等情况下,数据的迁移也会进行
  • PyDictObject缓冲池:与PyListObject缓冲池类似
0 0