如何实现Python的内置词典

时间:2008-11-29 07:35:31

标签: python data-structures dictionary

有谁知道如何实现python的内置字典类型?我的理解是它是某种哈希表,但我找不到任何确定的答案。

3 个答案:

答案 0 :(得分:398)

以下是我能够整理的所有关于Python dicts的内容(可能比任何人都想知道的更多;但答案是全面的)。

  • Python词典实现为哈希表
  • 哈希表必须允许哈希冲突,即使两个不同的键具有相同的哈希值,表的实现必须具有明确插入和检索键和值对的策略。
  • Python dict使用开放式寻址来解决哈希冲突(如下所述)(请参阅dictobject.c:296-297)。
  • Python哈希表只是一个连续的内存块(有点像数组,所以你可以通过索引进行O(1)查找)。
  • 表格中的每个广告位都只能存储一个条目。这很重要。
  • 表格中的每个条目实际上是三个值的组合:<哈希,键,值> 。这是作为C结构实现的(参见dictobject.h:51-56)。
  • 下图是Python哈希表的逻辑表示。在下图中,左侧的0, 1, ..., i, ...是散列表中插槽的索引(它们仅用于说明目的,并且不会与表一起存储!)。

    # 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取决于密钥的哈希
  • 如果该插槽为空,则该条目将添加到插槽中(通过输入,我的意思是<hash|key|value>)。但如果那个插槽被占用怎么办?很可能是因为另一个条目具有相同的哈希值(哈希冲突!)
  • 如果插槽被占用,CPython(甚至是PyPy)会比较哈希和键(通过比较我的意思是==比较而不是is比较)插槽中的条目分别针对要插入的当前条目的哈希和密钥(dictobject.c:337,344-345)。如果两者匹配,则认为该条目已存在,放弃并继续前进到要插入的下一个条目。如果散列或密钥不匹配,则启动探测
  • 探测只是意味着它按插槽搜索插槽以找到空插槽。从技术上讲,我们可以逐个进行i+1, i+2, ...并使用第一个可用的(线性探测)。但由于在评论中详细解释的原因(见dictobject.c:33-126),CPython使用随机探测。在随机探测中,以伪随机顺序拾取下一个时隙。该条目将添加到第一个空槽中。对于此讨论,用于选择下一个时隙的实际算法并不重要(有关探测算法,请参阅dictobject.c:33-126)。重要的是探测插槽直到找到第一个空插槽。
  • 同样的事情发生在查找中,只是从初始插槽i开始(其中i取决于密钥的散列)。如果散列和密钥都与插槽中的条目不匹配,则它开始探测,直到找到具有匹配的插槽。如果所有插槽都耗尽,则报告失败。
  • 顺便说一句,dict如果已满三分之二将会调整大小。这可以避免减慢查找速度。 (见dictobject.h:64-65

注意:我对Python Dict实现进行了研究,以响应我自己的question关于dict中的多个条目如何具有相同的哈希值。我在这里发布了一个略微编辑的回复版本,因为所有的研究都与这个问题非常相关。

答案 1 :(得分:44)

Python词典使用Open addressingreference inside Beautiful code

NB! 开放式寻址,又名封闭哈希应该如维基百科所述,不要与其相反的开放哈希相混淆!

打开寻址意味着dict使用数组槽,并且当在dict中获取对象的主要位置时,使用“扰动”方案在同一数组中的不同索引处搜索对象的点,其中对象的哈希价值发挥作用。

答案 2 :(得分:35)

  

Python的内置词典是如何实现的?

这是短期课程:

  • 它们是哈希表。
  • 从Python 3.6开始,新的程序/算法就是这样做的
    • 按键插入排序,
    • 占用更少的空间,
    • 几乎没有性能成本。
  • 当dicts共享密钥时(在特殊情况下),另一个优化可以节省空间。

从Python 3.6开始,有序方面是非官方的,但是official in Python 3.7

Python的词典是哈希表

很长一段时间,它的确如此。 Python将预先分配8个空行并使用哈希来确定键值对的位置。例如,如果键的散列以001结尾,则会将其粘贴在1索引中(如下例所示。)

     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进程计算不同的哈希值。)

上面描述的浪费空间使我们修改了字典的实现,带有一个令人兴奋的新(如果是非官方的)功能,现在订购了字典(通过插入)。

新契约哈希表

相反,我们通过为插入索引预先分配数组来开始。

由于我们的第一个键值对进入第二个插槽,我们的索引如下:

[null, 0, null, null, null, null, null, null]

我们的表只是按插入顺序填充:

     hash         key    value
...010001    ffeb678c    633241c4 
      ...         ...    ...

因此,当我们查找一个键时,我们使用哈希来检查我们期望的位置(在这种情况下,我们直接指向数组的索引1),然后转到哈希表中的那个索引(例如索引0),检查密钥是否相等(使用前面描述的相同算法),如果是,则返回值。

我们保持不变的查找时间,在某些情况下会有轻微的速度损失,而在其他情况下会有所增加,我们可以在预先存在的实施中节省相当多的空间。浪费的唯一空间是索引数组中的空字节。

Raymond Hettinger在2012年12月向python-dev介绍了这一点。它最终进入了Python 3.6的CPython。通过插入排序仍然被认为是一个实现细节,以允许Python的其他实现有机会赶上。

共享密钥

节省空间的另一个优化是共享密钥的实现。因此,我们没有冗余字典占用所有空间,而是使用重用共享密钥和密钥哈希的字典。你可以这样想:

     hash         key    dict_0    dict_1    dict_2...
...010001    ffeb678c    633241c4  fffad420  ...
      ...         ...    ...       ...       ...

对于64位计算机,每个额外字典每个密钥最多可以节省16个字节。

自定义对象的共享密钥&amp;替代

这些共享密钥dicts旨在用于自定义对象“__dict__。要获得此行为,我相信您需要在实例化下一个对象(see PEP 412)之前完成填充__dict__。这意味着您应该在__init____new__中分配所有属性,否则可能无法节省空间。

但是,如果您在执行__init__时了解所有属性,则还可以为对象提供__slots__,并保证根本不会创建__dict__ (如果在父母身上不可用),或者甚至允许__dict__,但保证您的预见属性无论如何都存储在插槽中。有关__slots__see my answer here

的更多信息

另见: