Answers:
这是我能够汇总的有关Python字典的所有内容(可能比任何人都想知道的要多;但是答案很全面)。
dict
使用开放式寻址来解决哈希冲突(如下所述)(请参阅dictobject.c:296-297)。O(1)
按索引进行查找)。下图是Python哈希表的逻辑表示。在下图中,0, 1, ..., i, ...
左侧是哈希表中插槽的索引(它们仅用于说明目的,与表显然没有一起存储!)。
# Logical model of Python Hash table
-+-----------------+
0| <hash|key|value>|
-+-----------------+
1| ... |
-+-----------------+
.| ... |
-+-----------------+
i| ... |
-+-----------------+
.| ... |
-+-----------------+
n| ... |
-+-----------------+
初始化新字典时,它从8 个插槽开始。(见dictobject.h:49)
i
,该槽基于键的哈希值。CPython最初使用i = hash(key) & mask
(where mask = PyDictMINSIZE - 1
,但这并不重要)。请注意,i
选中的初始插槽取决于密钥的哈希值。<hash|key|value>
)。但是,如果那个插槽被占用!?最可能是因为另一个条目具有相同的哈希(哈希冲突!)==
比较而不是is
比较)与要插入的当前条目的哈希值和键(dictobject.c)进行比较。 :337,344-345)。如果两者都匹配,则认为该条目已存在,放弃并继续下一个要插入的条目。如果哈希或密钥不匹配,则开始探测。i+1, i+2, ...
使用,然后使用第一个可用的(线性探测)。但是由于注释中详细解释的原因(请参阅dictobject.c:33-126),CPython使用了随机探测。在随机探测中,以伪随机顺序选择下一个时隙。该条目将添加到第一个空插槽。对于此讨论,用于选择下一个时隙的实际算法并不是很重要(有关探测的算法,请参见dictobject.c:33-126)。重要的是对插槽进行探测,直到找到第一个空插槽为止。dict
如果三分之二满了,它将被调整大小。这样可以避免减慢查找速度。(见dictobject.h:64-65)注意:我对Python Dict的实现进行了研究,以回答我自己的问题:字典中的多个条目如何具有相同的哈希值。我在此处发布了对此回复的略作修改的版本,因为所有的研究也都与此问题相关。
Python的内置词典如何实现?
这是短期课程:
从Python 3.6开始,有序方面是非官方的(给其他实现一个跟上的机会),但在Python 3.7中却是正式的。
长期以来,它完全像这样工作。Python将预分配8个空行,并使用哈希值确定键值对的粘贴位置。例如,如果密钥的哈希值以001结尾,则它将其保留在1(即2nd)索引中(如以下示例所示)。
<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 。通过插入排序被认为是3.6的实现细节,以使Python的其他实现有机会赶上。
节省空间的另一种优化方法是共享密钥的实现。因此,我们没有重复使用共享密钥和密钥散列的冗余字典,而不是拥有占据所有空间的冗余字典。您可以这样想:
hash key dict_0 dict_1 dict_2...
...010001 ffeb678c 633241c4 fffad420 ...
... ... ... ... ...
对于64位计算机,每个额外的字典每个键最多可以节省16个字节。
这些共享密钥字典旨在用于自定义对象__dict__
。为了获得这种行为,我相信您需要__dict__
在实例化下一个对象之前完成填充(请参阅PEP 412)。这意味着您应该在__init__
或中分配所有属性__new__
,否则可能无法节省空间。
但是,如果您知道执行时的所有属性,则__init__
还可以提供__slots__
对象,并保证__dict__
根本不会创建该对象(如果在父级中不可用),甚至允许__dict__
但保证您可以预见的属性是仍然存储在插槽中。有关更多信息__slots__
,请在此处查看我的答案。
**kwargs
。find_empty_slot
:github.com/python/cpython/blob/master/Objects/dictobject.c #L969-从134行开始,有一些散文对此进行了描述。
Python字典使用Open寻址(在Beautiful代码中参考)
注意! 如Wikipedia所述,开放式寻址(又名封闭式哈希)不应与其相反的开放式哈希混淆!
开放式寻址意味着dict使用数组插槽,并且当对象的主要位置位于dict中时,使用“扰动”方案在同一数组中的不同索引处查找对象的位置,其中对象的哈希值发挥了作用。