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

源代码选用 最多见的 cpythonpython

首先来看看构建dict的基础设施:算法

typedef struct {
Py_ssize_t me_hash;
PyObject *me_key;
PyObject *me_value;
} PyDictEntry;函数

这个结构体为dict中key-value,其中的me_hash为me_key的hash值,[空间换时间]。除此以外,咱们发现me_key与me_value都是PyObject指针类型,这也说明了为何dict中的key与value能够为python中的任何类型数据。spa

struct _dictobject {
PyObject_HEAD
Py_ssize_t ma_fill;
Py_ssize_t ma_used;
Py_ssize_t ma_mask;
PyDictEntry *ma_table;
PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
PyDictEntry ma_smalltable[PyDict_MINSIZE];
};设计

这个结构体即是dict了。按照咱们一般的理解,dict应该是可变长对象啊!为何这里还有PyObject_HEAD,而不是PyObject_VAR_HEAD。仔细一看,dict的可变长与 string,list,tuple 仍有不一样以外,后者能够经过PyObject_VAR_HEAD中的ob_size来指明其内部有效元素的个数。但dict不能这样作,因此dict干脆绕开PyObject_VAR_HEAD,并且除了有ma_used这个字段来交代出其有效元素的个数,还须要ma_fill来交代清楚曾经有效元素的个数(用来计算加载率)。指针

ma_mask,则牵扯到hash中的散列函数;
ma_smalltable,python一贯的有限空间换时间,一个小池子来应付大多数的小dict(不超过PyDict_MINSIZE);
ma_lookup,则是一次探测与二次探测函数的实现。对象

在展开dict实现细节前,先把dict使用的解决冲突的开放定址法介绍一下。咱们知道哈希,就是将一个无限集合映射到一个有限集,若是选择理想的hash函数,可以将预期处理到的元素均匀分布到有限集中便可在O(1)时间内完成元素查找。但理想的hash函数是不存在的,且因为映射的本质(无限到有限)必然出出现一个位置有多个元素要‘占据’,这就须要解决冲突。现有的解决冲突的方法:内存

  1. 开放定址法
  2. 链地址法
  3. 多哈希函数法
  4. 建域法

其中建域法基本思想为假设哈希函数的值域为[0,m-1],则设向量HashTable[0..m-1]为基本表,另外设立存储空间向量OverTable[0..v]用以存储发生冲突的记录。源码

其中前两种方法实现最为简单高效,下面回顾下开放定址与链地址法。string

开放定址法:造成hash表时,某元素在第一次探测其应该占有的位置时,若是发现此处(记为A)已经被别人占了,那就在从A开始,再次探测(固然此次探测使用的hash函数与第一次已经不同了),若是发现仍是被别人占了,那么继续探测,至到找到一个可用位置(也有可能在当下条件下永远找不到)。开放地址法有一个相当重要的问题须要解决,那就是在一个元素离开hash表时,如何处理离开后的位置状态。若是设置为原始空状态,那么后续的有效元素就没法识别了,由于在查找时一样是依据上面的探测规则进行查找,因此必须告诉探测函数某个位置虽然无有效元素了,但后续的探测可能会出现有效元素。咱们能够发现,开放定址法很容易发生冲突(主要是一次探测以上成功的元素占取其它元素应该在第一次探测成功的位置),因此就须要加大hash有效空间。

链地址法:链地址法的思想很简单,你不是可能会出现多个元素对应同一个位置,那么我就在这个位置拉出一个链表来存放因此hash到这个位置的元素。很简单吧,还节约内存呢!很遗憾,python的设计者没有选它。

那为何python发明者选择了开放定址而不是链地址法,在看python源码时看到这么一段话:

Open addressing is preferred over chaining since the link overhead(开销) for chaining would be substantial(大量) (100% with typical malloc overhead).

因为链地址法须要动态的生成链表结点(malloc),因此时间效率不如开放定址法(但开放定址法的装载率不能高于2/3,相对于链地址法的空间开销也是毋庸置疑的),由此能够看出python的设计时代已经不是那个内存只有512k可供使用的时代了,对内存的苛刻已经让步于效率。固然这须要考虑到python因为实现动态而必须靠自身的设计将损失的时间效率尽量地补回来。

好了,交待完开放定址法与为何python设计者选择它后,咱们来看看dict如何实现这个算法的。前面已经看到每一个key-value由一个Entry结构体实现,python就是利用entry自身的信息来指明每一个位置的状态:原始空状态、有效元素离去状态、有效元素占据状态。

  • 原始空:me_key:Null ;me_value:Null
  • 有效元素离去:me_key:dummy; me_value:Null
  • 有效元素占据:me_key:not Null and not dummy ;me_value:not Null

其中dict的hash方法与冲突解决方法的思路以下:

lookdict(k,v)

  1. index <- hash1(k),freeslot<-Null,根据me_key与me_value选择二、三、4一个执行;
  2. 查看index处的值处于’有效元素占据‘状态,判断data[index]与v是否一致(地址或内容),一致,则返回查找成功;不然转5
  3. index所指向的位置处于’原始空‘状态,查找失败,若freeslot==Null返回index;不然返回freeslot;转5
  4. index所指向的位置处于’有效元素离去‘状态,freeslot<-index, 转5
  5. index <- hash2(index),,转2

dict的lookdict方法实现充分体现了python对内存的利用率与空间换时间提升效率上,表现为以下方面:

    1. 内存利用率:当找到原始空状态时,若是前面已经找到dummy态的entry,则会将其返回。
    2. 提升效率:ma_table始终指向有效散列空间的开始位置,在开辟新空间后,small_table就弃之不用了,ma_table改指向新开辟空间的首位置。
相关文章
相关标签/搜索