本文描述了Python是如何实现dictionary。dictionary是由主键key索引,可以被看作是关联数组,类似于STL的map。有如下的基本操作。
|
|
|
|
|
|
|
|
|
|
元素的访问方式如下:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
如果key不存在,将会返回KeyError的异常。
Python中dictionary是以hash表实现,而hash表以数组表示,数组的索引作为主键key。Hash函数的目标是key均匀的分布于数组中,良好的hash函数能够最小化减少冲突的次数。
在本文中,我们采用string作为key为例子。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
如果你执行hash(‘a’),函数将会执行string_hash()返回12416037344,在此我们假设采用的是64位系统。
假设数组的大小是x, 该数组用来存储key/value, 我们用掩码x-1计算这个数组的索引。例如,如果数组的大小是8,‘a’的索引是hash(‘a’)&7=0, ‘b’的索引是3, ‘c’的索引是2。’z’的索引是3与’b’的索引一致,由此导致冲突。
我们看到Python中的hash函数在key是连续的时候表现很好的性能,原因是它对于处理这种类型的数据有通用性。但是,一旦我们加入了’z’,由于数据的不连贯性产生了冲突。
当产生冲突的时候,我们可以采用链表来存储这对key/value,但是这样增加了查找时间,不再是O(1)的复杂度。在下一节中,我们将要具体描述一下Python中dictionary解决这种冲突的方法。
散列地址方法是解决hash冲突的探测策略。对于’z’,由于索引3已经被占用,所以我们需要寻找一个没有使用的索引,这样添加key/value的时候肯能花费更多的时间,但是查找时间依然是O(1),这是我们所期望的结果。
这里采用quadratic探测序列来寻找可用的索引,代码如下:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
让我们来看看它是如何工作的,令i=3
3 -> 3 -> 5 -> 5 -> 6 -> 0…
索引5将被用作’z’的索引值,这不是一个很好的例子,因为我们采用了一个大小是8的数组。对于大数组,算法显示它的优势。
出于好奇,我们来看看当数组大小是32,mask是31时,探测序列为
3 -> 11 -> 19 -> 29 -> 5 -> 6 -> 16 -> 31 -> 28 -> 13 -> 2…
如果你想了解更多,可以察看dictobject.c的源代码。
接下来,让我们看看Python内部的代码是如何实现的?
如下的C语言结构体用来存储一个dictionary条目:key/value对,结构体内有Hash值,key值和value值。PyObject是Python对象的基类。
|
|
|
|
|
|
|
|
|
|
|
|
如下的结构体代表了一个dictionary。ma_fill是已使用slot与未使用slot之和。当一对key/value被删除了,该slot被标记为未使用。ma_used是已使用slot的数目。ma_mask等于数组大小减一,用来计算slot的索引。ma_table是该数组,ma_smalltable是初始数组大小为8.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
当我们第一次创建一个dictionary的时候,将会调用PyDict_New()。我们以伪码表示该函数的主要功能。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
当增加一对新的key/value时,将调用PyDict_SetItem()。该函数的参数有dictionary对象的指针和key/value对。它检测key是否为字符串以及计算hash值或者重复使用已经存在的index。Insertdict()用来增加新的key/value,在dictionary的大小被重新调整之后,而且已使用slot的数量超过数组大小的2/3.
为什么是2/3?这样能够保证probing序列能够快速地找到空闲的slot。稍后我们将看看调整数组大小的函数。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Insertdict()使用查找函数来找到一个未使用的slot。接下来我们来看看这个函数,lookdict_string()利用hash值和掩码来计算slot的索引。如果它找不到slot索引的key等于hash&mask,它会使用扰码来探测。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
我们在dictionary中增加如下的key/value:{‘a’: 1, ‘b’: 2′, ‘z’: 26, ‘y’: 25, ‘c’: 5, ‘x’: 24}。流程如下:
A dictionary structure is allocated with internal table size of 8.
This is what we have so far:
由于8个slot中的6已经被使用了即超过了数组容量的2/3,dictresize()被调用来分配更大的内存,该函数同时拷贝旧数据表的数据进入新数据表。
在这个例子中,dictresize()调用时,minused是24也就是4×ma_used。当已使用slot的数目很大的时候(超过50000)minused是2×ma_used。为什么是4倍?这样能够减少调整步骤的数目而且增加稀疏度。
v新数据表的大少应该大于24,它是通过把当前数据表的大小左移一位实现的,直到她大于24.例如 8 -> 16 -> 32.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
当数据表大小进行调整的时候所发生的:分配了一个大小是32的新数据表
函数PyDict_DelItem()用来删除一个条目。计算key的hash值,然后调用查找函数返回这个条目。该条目的key设置为哑元。这个哑元包含了过去使用过的key值但现在还是未使用的状态。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
如果我要从dictionary中删除key ’c’,将以如下的数组结束。
注意:删除操作并没有改变数组的大小,即使已使用slot的数目远小于slot的总数目。
Thanks