关于数据结构:如何实现Python的内置字典

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用于自定义对象的__dict__。为了实现这种行为,我认为您需要在实例化下一个对象之前完成对__dict__的填充(参见pep 412)。这意味着您应该在__init____new__中分配所有属性,否则可能无法节省空间。好的。

但是,如果您在执行__init__时知道所有的属性,您还可以为您的对象提供__slots__,并保证根本不创建__dict__(如果在父级中不可用),甚至允许__dict__,但保证您的可预见属性以任何方式存储在槽中。有关__slots__的更多信息,请参阅我的回答。好的。参见:

  • PEP509——在dict中添加一个私有版本
  • pep 468——保留函数中**kwargs的顺序。
  • PEP 520—保留类属性定义顺序
  • 2010年:五月字典-布兰登·罗德斯
  • Pycon 2017:字典更强大-布兰登·罗德斯
  • Pycon 2017:现代Python词典汇集了许多伟大的思想-Raymond Hettinger
  • dictobject.c-cpython在c中的实际dict实现。

好啊。