How are Python's Built In Dictionaries Implemented
有人知道如何实现Python的内置字典类型吗?我的理解是它是某种哈希表,但我还没有找到任何确定的答案。
下面是关于python dicts的所有内容,我可以将它们放在一起(可能比任何人都想知道的多;但答案是全面的)。
- python字典被实现为散列表。
- 哈希表必须允许哈希冲突,即即使两个不同的键具有相同的哈希值,该表的实现也必须有一个策略来明确地插入和检索键和值对。
- python
dict 使用开放寻址来解决散列冲突(如下所述)(参见dictobject.c:296-297)。 - python散列表只是一个连续的内存块(有点像数组,所以可以按索引进行
O(1) 查找)。 - 表中的每个插槽只能存储一个条目。这很重要。
- 表中的每个条目实际上是三个值的组合:
。这是作为C结构实现的(参见dictobject.h:51-56)。 下图是Python哈希表的逻辑表示。在下图中,左边的
0, 1, ..., i, ... 是哈希表中插槽的索引(它们只是为了说明目的,显然不与表一起存储!).1
2
3
4
5
6
7
8
9
10
11
12
13
14# Logical model of Python Hash table
-+-----------------+
0| <hash|key|value>|
-+-----------------+
1| ... |
-+-----------------+
.| ... |
-+-----------------+
i| ... |
-+-----------------+
.| ... |
-+-----------------+
n| ... |
-+-----------------+当一个新的dict被初始化时,它从8个槽开始。(见dictobject.h:49)
- 在向表中添加条目时,我们从一些槽开始,即基于键散列的
i 。cpython最初使用i = hash(key) & mask (其中mask = PyDictMINSIZE - 1 ,但这并不重要)。只需注意,检查的初始插槽i 取决于密钥的散列值。 - 如果该插槽为空,则会将该条目添加到该插槽中(通过条目,我的意思是,
)。但如果那个槽被占了呢!?很可能是因为另一个条目具有相同的哈希(哈希冲突!) - 如果插槽被占用,cpython(甚至pypy)将插槽中条目的哈希和键(通过比较,我的意思是
== 比较,而不是is 比较)与要插入的当前条目的哈希和键(dictobject.c:337344-345)分别进行比较。如果两者都匹配,那么它认为条目已经存在,放弃并移动到要插入的下一个条目。如果哈希或键不匹配,则开始探测。 - 探测只意味着它逐槽搜索插槽以找到空插槽。从技术上讲,我们可以一个接一个地使用第一个可用的(即线性探测)。但是由于评论中解释得很好的原因(见dictobject.c:33-126),cpython使用随机探测。在随机探测中,下一个槽是以伪随机顺序选取的。该条目将添加到第一个空槽中。对于这个讨论,用于选择下一个槽的实际算法实际上并不重要(关于探测算法,请参见dictobject.c:33-126)。重要的是要探测插槽,直到找到第一个空插槽。
- 查找也会发生同样的事情,只需从初始槽I开始(在这里我依赖于键的散列值)。如果散列和密钥都不匹配槽中的条目,它将开始探测,直到找到匹配的槽。如果所有插槽都用尽,则报告失败。
- 顺便说一句,如果
dict 已满三分之二,它将被调整大小。这样可以避免减慢查找速度。(见dictobject.h:64-65)
注意:我对python dict实现进行了研究,以响应我自己关于dict中多个条目如何具有相同哈希值的问题。我在这里发布了一个稍微经过编辑的回复,因为所有的研究也与这个问题非常相关。
python字典使用开放式寻址(在漂亮的代码中引用)
NB!正如维基百科所指出的,开放寻址,即封闭散列,不应与相反的开放散列混淆!
开放寻址意味着dict使用数组槽,当一个对象的主位置在dict中被取下时,该对象的点在同一数组中的不同索引处被寻找,使用"扰动"方案,对象的散列值在其中起作用。
How are Python's Built In Dictionaries Implemented?
Ok.
以下是简短的课程:好的。
- 它们是哈希表。
- 从python 3.6开始,一个新的过程/算法使它们
- 按插入键排序,以及
- 占用更少的空间,
- 在性能上几乎没有成本。
- 当dict共享密钥时(在特殊情况下),另一个优化可以节省空间。
从python 3.6开始,有序的方面是非官方的,但在python3.7中是官方的。好的。python的字典是散列表
很长一段时间以来,它都是这样工作的。python将预先分配8个空行,并使用散列来确定将键值对粘贴到哪里。例如,如果键的哈希以001结尾,它将把它粘贴到1索引中(如下面的示例)。好的。
1 2 3 4 5 | hash key value null null null ...010001 ffeb678c 633241c4 # addresses of the keys and values null null null ... ... ... |
每行在64位体系结构上占用24个字节,在32位上占用12个字节。(请注意,列标题只是标签—它们实际上不存在于内存中。)好的。
如果散列的结尾与先前存在的键的散列相同,则这是一个冲突,然后它会将键值对粘贴到不同的位置。好的。
在存储了5个键值之后,在添加另一个键值对时,哈希冲突的概率太大,因此字典的大小增加了一倍。在64位过程中,在调整大小之前,我们有72个字节是空的,之后,由于10个空行,我们浪费了240个字节。好的。
这需要很大的空间,但是查找时间是相当恒定的。密钥比较算法是计算散列值,转到预期的位置,比较密钥的ID——如果它们是相同的对象,那么它们是相等的。如果不是,那么比较散列值,如果散列值不相同,那么它们就不相等。否则,我们最后比较键是否相等,如果相等,则返回值。相等的最终比较可能非常缓慢,但早期的检查通常会缩短最终比较的速度,使查找非常迅速。好的。
(冲突会降低速度,理论上攻击者可以使用哈希冲突来执行拒绝服务攻击,因此我们将哈希函数随机分组,以便它为每个新的python进程计算不同的哈希。)好的。
上面描述的浪费空间导致我们修改了字典的实现,其中有一个令人兴奋的新特性(如果非官方的话),字典现在是按插入顺序排列的。好的。新的压缩哈希表
相反,我们首先为插入的索引预先分配一个数组。好的。
因为我们的第一对键值进入第二个槽,所以我们的索引如下:好的。
1 | [null, 0, null, null, null, null, null, null] |
我们的表只是按插入顺序填充:好的。
1 2 3 | hash key value ...010001 ffeb678c 633241c4 ... ... ... |
因此,当我们查找一个键时,我们使用哈希来检查我们期望的位置(在本例中,我们直接转到数组的索引1),然后转到哈希表中的索引(例如索引0),检查键是否相等(使用前面描述的相同算法),如果是,返回值。好的。
我们保持不变的查找时间,在某些情况下速度损失很小,而在其他情况下速度增加,这样做的好处是,我们比以前的实现节省了很多空间。唯一浪费的空间是索引数组中的空字节。好的。
Raymond Hettinger在2012年12月向python dev介绍了这一点。它最终在python 3.6中进入了cpython。按插入排序仍然被认为是一个实现细节,以便让其他的Python实现有机会赶上。好的。共享密钥
另一个节省空间的优化是共享密钥的实现。因此,我们没有使用占用所有空间的冗余字典,而是使用重用共享键和键散列的字典。你可以这样想:好的。
1 2 3 | hash key dict_0 dict_1 dict_2... ...010001 ffeb678c 633241c4 fffad420 ... ... ... ... ... ... |
对于64位机器,每个额外的字典每键最多可以保存16个字节。好的。自定义对象和备选方案的共享键
这些共享密钥dict用于自定义对象的
但是,如果您在执行
- PEP509——在dict中添加一个私有版本
- pep 468——保留函数中
**kwargs 的顺序。 - PEP 520—保留类属性定义顺序
- 2010年:五月字典-布兰登·罗德斯
- Pycon 2017:字典更强大-布兰登·罗德斯
- Pycon 2017:现代Python词典汇集了许多伟大的思想-Raymond Hettinger
- dictobject.c-cpython在c中的实际dict实现。
好啊。