OrderedDict 是如何保證 Key 的插入順序的?

???? Python貓” ,一個值得加星標的公衆號

劇照 | 《霸王別姬》

作者:oldPanda

來源:https://old-panda.com/2020/02/09/python-ordereddict

現象

根據哈希表的定義,以及之前簡單實現過的一個字典數據結構,當 Key 被插入哈希表後,哈希表根據散列函數求出的值來安排這個 Key 所在的位置,所以當我們遍歷哈希表的時候, Key 的順序是不確定的,因此碼農在使用哈希表這個數據結構的時候,是不應該依賴於 Key 的插入順序來達到某些目的的。

但有的時候我們可能會對哈希表中 Key 的插入順序感興趣,這時有經驗的 Python 工程師就會用 collections 中的 OrderedDict 來保持插入 Key 的順序。

>>> d1 = {}
>>> d1['a'] = 1
>>> d1['b'] = 2
>>> d1['c'] = 3
>>> d1['d'] = 4
>>> d1['e'] = 5
>>> d1
{'b': 2, 'd': 4, 'c': 3, 'a': 1, 'e': 5}
>>> from collections import OrderedDict
>>> d2 = OrderedDict()
>>> d2['a'] = 1
>>> d2['b'] = 2
>>> d2['c'] = 3
>>> d2['d'] = 4
>>> d2['e'] = 5
>>> d2
OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)])
>>>

d1 是一個普通的字典實例,我們按照 abcde 的順序插入 Key 之後,打印 d1 顯示的是亂序的,這符合我們對哈希表的理解, d2 是一個 OrderedDict 實例,同樣按 abcde 的順序插入,從輸出結果我們能看到它記憶了插入的順序。那麼問題來了,它是如何做到的?

原理

還是去源碼中一探究竟,找到 Python 3 的 OrderedDict 實現。

不難發現,在繼承 了自身的 dict 之後,類中還聲明瞭一個循環雙向鏈表 self.__root 作爲自己的屬性,用另一個 dict 結構 self.__map 來存儲 Key 到鏈表節點的對應關係,這樣在更新或刪除一個已有 Key 的時候,能在常數時間內完成對雙向鏈表的操作。

這幾個數據結構的關係如下所示,注意 self.__root 總是指向雙向鏈表的頭部。

這樣在遍歷 OrderedDict 的時候,直接過一遍循環雙向鏈表即可。

但類變得複雜了,相應的操作也會複雜起來,但上述的兩種數據結構的結合,仍然能夠做到對字典示例常數級操作。查詢( __getitem__ )操作比較直觀,可以通過調用父類的函數來完成。下面重點記錄插入和刪除操作是如何進行的。

插入

插入操作調用魔術方法 __setitem__ ,當 Key 存在時,直接調用父類函數,複雜的情況是當 Key 不存在的時候,不僅需要把鍵值對放到哈希表中,還要同步更新 self.__mapself.__root

  1. 初始化新 Key 的鏈表節點

  2. 找到雙向鏈表的尾端,因爲是循環鏈表,所以可以直接通過 self.__root.prev 得到

  3. 將新的鏈表節點放到雙向鏈表的尾端

  4. 調用父類的插入函數處理新的鍵值對

刪除

刪除操作調用 __delitem__

  1. 調用父類的刪除函數刪掉給定的 Key

  2. 通過 self.__map 定位到將要刪除的 Key 對應的鏈表節點

  3. 從鏈表中將這個節點刪除

顯然,關於有順序的哈希表,核心是對循環雙向鏈表的操作,而鏈表的插入刪除都是常數級操作,又用了一個 dict 來保證查詢的常數級操作,所以既能記錄 Key 插入的順序,又能保證操作的時間複雜度。

但是。。。

在 Python 3.7 的發佈中,官宣了 Python 原生的 dict 就能保證 Key 的插入順序。

The insertion-order preservation nature of dict objects is now an official part of the Python language spec.

這是否就意味着 OrderedDict 變得多餘了呢?StackOverflow 上有個討論列出了 Python 3.7 中原生 dictOrderedDict 的區別,主要有兩條。

  • OrderedDict 支持對 Key 的順序有關的操作,比如說把某個 Key 挪到頭部或者尾部,逆序給出 Key 的序列(這個特性在 Python 3.8 中加到了原生 dict 中)等

  • OrderedDict 比較的時候會把 Key 的插入順序考慮進去,而 dict 不會,比如說

>>> from collections import OrderedDict 
>>> OrderedDict([(1,1), (2,2)]) == OrderedDict([(2,2), (1,1)]) 
False 
>>> dict([(1,1), (2,2)]) == dict([(2,2), (1,1)]) 
True

優質文章,推薦閱讀:

Python 項目提速技巧:連接複用

如何通過測試提升 Python 代碼的健壯性

技術人都應該瞭解的一種數據格式——JSON

Python 高速增長的三次歷史機遇

感謝創作者的好文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章