本文主要翻譯自 so 上面的問題 Why can a Python dict have multiple keys with the same hash? 下 Praveen Gollakota 的答案
Python 字典是通過哈希表實現的
哈希表必然存在哈希沖突。比如:就算兩個鍵存在相同的哈希值,哈希表必須要有策略用來明確兩個值插入和讀取
Python 字典使用開放尋址法解決哈希沖突(下面展開講)(源碼:dictobject.c:296-297)
Python 的哈希表僅僅是一塊連續的內存(類似于數組,因此可以使用索引進行 O(1) 的查找)
表里的每個插槽只能存儲一個 entry,這是很重要的
表里的每個 entry 實際上存儲了三個值,這是由 C 結構實現的(詳見 dictobject.h:51-56)
-
下面是 Python 哈希表的邏輯示例圖,0,1,...,i,... 這些數是對插槽的索引(僅僅只是為了說明,實際上它們并沒有與表格一起存放)
# Logical model of Python Hash table -+-----------------+ 0| <hash|key|value>| -+-----------------+ 1| ... | -+-----------------+ .| ... | -+-----------------+ i| ... | -+-----------------+ .| ... | -+-----------------+ n| ... | -+-----------------+
新字典初始化時擁有 8 個插槽(見 dictobject.h:49)
當往哈希表中添加 entry 時,我們以一些插槽開始,比如 i,它是基于對鍵的哈希。Cpython 使用
i = hash(key) & mask
初始化(這里mask = PyDictMINSIZE - 1
,但這不是重點),注意初始值 i 取決于對鍵的哈希如果該插槽是空的,entry 將會被添加到插槽中(entry 即
<hash|key|value>
),如果插槽已經被占用時怎么辦呢?這常常是由于其它的 entry 擁有相同的哈希值(即哈希沖突)如果插槽被占用,CPython(包括 PyPy)會對比已占用的和將被插入的 entry 的哈希值和鍵(使用
==
對比而不是is
)(見:dictobject.c:337,344-345),如果兩個都相同,則認為這個 entry 已經存在,繼而轉向下一個被插入的 entry。如果存在哈希和鍵中某一個不匹配,則會開始查找查找意味它會一個一個的查看插槽是否為空,以找到一個空的插槽。技術上來說,我們可以通過不斷加 1,如 i+1,i+2,...一旦找到可用的就停止(即線性查找)。但是,因為某些原因(源代碼的注釋非常漂亮的闡明了這些原因,見 dictobject.c:33-126),CPython 使用了隨機查找。在隨機查找中,下一個插槽的位置是一個偽隨機數,而 entry 也會被添加到找到的第一個空的插槽中。具體的算法對于本次討論來說并不太重要(具體可以查看 dictobject.c:33-126)。重要的是當第一個空插槽被找到時,查找則停止
同樣的事情也發生在索引的時候,它始于初始化的值 i(i 取決于鍵的哈希值),如果對應的插槽所在的 entry 哈希值和鍵都不匹配,則會開始查找,直到找到一個匹配的插槽。如果所有的插槽都找遍了也沒有找到匹配的,則會報告錯誤
另外,字典將會在占用了 2/3 的時候重新調整大小,這會避免降低查找的速度(見 dictobject.h:64-65)
實際測試效果如下:
class HashTester(object):
def __init__(self):
self.value = 42
def __hash__(self):
return self.value
def __eq__(self, other):
return self.value == other.value
class HashTester2(object):
def __hash__(self):
return 42
>>> a = HashTester()
>>> b = HashTester()
>>> {a: 'this is a', b: 'this is b'} # a 與 b 的 hash 和 key 都相等
{<__main__.HashTester object at 0x00000222B7A691C0>: 'this is b'}
>>> e = HashTester2()
>>> f = HashTester2()
>>> {e: 'this is e', f: 'this is f'} # e 與 f 哈希沖突
{<__main__.HashTester2 object at 0x00000222B7A69CD0>: 'this is e', <__main__.HashTester2 object at 0x00000222B7A690A0>: 'this is f'}