1. 程式人生 > >流暢的python 第三張 字典和集合

流暢的python 第三張 字典和集合

形式 mce contain 接口 行為 字符 同時 值對象 用戶創建

介紹

dict 類型不但在各種程序裏廣泛使用,它也是 Python 語言的基石。模塊的命名空間、實例的屬性和函數的關鍵字參數中都可以看到字典的身影。跟它有關的內置函數都在 __builtins__.__dict__模塊中。


正是因為字典至關重要,Python 對它的實現做了高度優化,而散列表則是字典類型性能出眾的根本原因。


集合(set)的實現其實也依賴於散列表,因此本章也會講到它。反過來說,想要進一步理解集合和字典,就得先理解散列表的原理。

泛映射類型

collections.abc 模塊中有 Mapping 和 MutableMapping 這兩個抽象基類,它們的作用是為 dict 和其他類似的類型定義形式接口(在Python 2.6 到 Python 3.2 的版本中,這些類還不屬於 collections.abc

模塊,而是隸屬於 collections 模塊)。

技術分享圖片

collections.abc 中的 MutableMapping 和它的超類的UML 類圖(箭頭從子類指向超類,抽象類和抽象方法的名稱以斜體顯示)

然而,非抽象映射類型一般不會直接繼承這些抽象基類,它們會直接對dict 或是 collections.User.Dict 進行擴展。這些抽象基類的主要作用是作為形式化的文檔,它們定義了構建一個映射類型所需要的最基
本的接口。然後它們還可以跟 isinstance 一起被用來判定某個數據是不是廣義上的映射類型:

>>> my_dict = {}
>>> isinstance(my_dict, abc.Mapping)
True

這裏用 isinstance 而不是 type 來檢查某個參數是否為 dict 類型,因為這個參數有可能不是 dict,而是一個比較另類的映射類型。

標準庫裏的所有映射類型都是利用 dict 來實現的,因此它們有個共同的限制,即只有可散列的數據類型才能用作這些映射裏的鍵(只有鍵有這個要求,值並不需要是可散列的數據類型)。

什麽是可散列的數據類型?

如果一個對象是可散列的,那麽在這個對象的生命周期中,它的散列值是不變的,而且這個對象需要實現 __hash__() 方法。另外可散列對象還要有 __qe__() 方法,這樣才能跟其他鍵做比較。如果兩個可散列對象是相等的,那麽它們的散列值一定是一樣的……



原子不可變數據類型(str、bytes 和數值類型)都是可散列類型,frozenset 也是可散列的,因為根據其定義,frozenset 裏只能容納可散列類型。元組的話,只有當一個元組包含的所有元素都是可散列類型的情況下,它才是可散列的。來看下面的元組tt、tl 和 tf:

>>> tt = (1, 2, (30, 40))
>>> hash(tt)
8027212646858338501
>>> tl = (1, 2, [30, 40])
>>> hash(tl)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: list
>>> tf = (1, 2, frozenset([30, 40]))
>>> hash(tf)
-4118419923444501110

一般來講用戶自定義的類型的對象都是可散列的,散列值就是它們
的 id() 函數的返回值,所以所有這些對象在比較的時候都是不相
等的。如果一個對象實現了 __eq__ 方法,並且在方法中用到了這
個對象的內部狀態的話,那麽只有當所有這些內部狀態都是不可變
的情況下,這個對象才是可散列的。

一般來講用戶自定義的類型的對象都是可散列的,散列值就是它們的 id() 函數的返回值,所以所有這些對象在比較的時候都是不相等的。如果一個對象實現了 __eq__ 方法,並且在方法中用到了這
個對象的內部狀態的話,那麽只有當所有這些內部狀態都是不可變的情況下,這個對象才是可散列的。

>>> a = dict(one=1, two=2, three=3)
>>> b = {one: 1, two: 2, three: 3}
>>> c = dict(zip([one, two, three], [1, 2, 3]))
>>> d = dict([(two, 2), (one, 1), (three, 3)])
>>> e = dict({three: 3, one: 1, two: 2})
>>> a == b == c == d == e
True

用setdefault處理找不到的鍵

當字典 d[k] 不能找到正確的鍵的時候,Python 會拋出異常,這個行為符合 Python 所信奉的“快速失敗”哲學。也許每個 Python 程序員都知道可以用 d.get(k, default) 來代替 d[k],給找不到的鍵一個默認的
返回值(這比處理 KeyError 要方便不少)。但是要更新某個鍵對應的值的時候,不管使用 __getitem__ 還是 get 都會不自然,而且效率低。dict.get 並不是處理找不到的鍵的最好方法。

"""創建一個從單詞到其出現情況的映射"""
import sys
import re
WORD_RE = re.compile(r\w+)
index = {}
with open(sys.argv[1], encoding=utf-8) as fp:
    for line_no, line in enumerate(fp, 1):
        for match in WORD_RE.finditer(line):
            word = match.group()
            column_no = match.start()+1
            location = (line_no, column_no)
            # 這其實是一種很不好的實現,這樣寫只是為了證明論點
            occurrences = index.get(word, []) ?
            occurrences.append(location) ?
            index[word] = occurrences ?
           # 以字母順序打印出結果
for word in sorted(index, key=str.upper): ?
print(word, index[word])

? 提取 word 出現的情況,如果還沒有它的記錄,返回 []。
? 把單詞新出現的位置添加到列表的後面。
? 把新的列表放回字典中,這又牽扯到一次查詢操作。
? sorted 函數的 key= 參數沒有調用 str.uppper,而是把這個方法的引用傳遞給 sorted 函數,這樣在排序的時候,單詞會被規範成統一格式。

通過 dict.setdefault 可以只用一行解決。

"""創建從一個單詞到其出現情況的映射"""
import sys
import re
WORD_RE = re.compile(r\w+)
index = {}
with open(sys.argv[1], encoding=utf-8) as fp:
    for line_no, line in enumerate(fp, 1):
        for match in WORD_RE.finditer(line):
            word = match.group()
            column_no = match.start()+1
            location = (line_no, column_no)
            index.setdefault(word, []).append(location) ?
            # 以字母順序打印出結果
for word in sorted(index, key=str.upper):
print(word, index[word])

? 獲取單詞的出現情況列表,如果單詞不存在,把單詞和一個空列表
放進映射,然後返回這個空列表,這樣就能在不進行第二次查找的情況
下更新列表了

也就是說,這樣寫:

my_dict.setdefault(key, []).append(new_value)

跟這樣寫:

if key not in my_dict:
my_dict[key] = []
my_dict[key].append(new_value)

二者的效果是一樣的,只不過後者至少要進行兩次鍵查詢——如果鍵不存在的話,就是三次,用 setdefault 只需要一次就可以完成整個操作。


在用戶創建 defaultdict 對象的時候,就需要給它配置一個為找不到的鍵創造默認值的方法。
具體而言,在實例化一個 defaultdict 的時候,需要給構造方法提供一個可調用對象,這個可調用對象會在 __getitem__ 碰到找不到的鍵的時候被調用,讓 __getitem__ 返回某種默認值。
比如,我們新建了這樣一個字典:dd = defaultdict(list),如果鍵‘new-key‘ 在 dd 中還不存在的話,表達式 dd[‘new-key‘] 會按照以下的步驟來行事。
(1) 調用 list() 來建立一個新列表。
(2) 把這個新列表作為值,‘new-key‘ 作為它的鍵,放到 dd 中。
(3) 返回這個列表的引用。
而這個用來生成默認值的可調用對象存放在名為 default_factory 的
實例屬性裏。

利用 defaultdict 實例而不是setdefault 方法

"""創建一個從單詞到其出現情況的映射"""
import sys
import re
import collections
WORD_RE = re.compile(r\w+)
index = collections.defaultdict(list) ?
with open(sys.argv[1], encoding=utf-8) as fp:
    for line_no, line in enumerate(fp, 1):
        for match in WORD_RE.finditer(line):
            word = match.group()
            column_no = match.start()+1
            location = (line_no, column_no)
            index[word].append(location) ?
            # 以字母順序打印出結果
for word in sorted(index, key=str.upper):
print(word, index[word])

? 把 list 構造方法作為 default_factory 來創建一個
defaultdict。
? 如果 index 並沒有 word 的記錄,那麽 default_factory 會被調用,為查詢不到的鍵創造一個值。這個值在這裏是一個空的列表,然後這個空列表被賦值給 index[word],繼而被當作返回值返回,因此
.append(location) 操作總能成功。

如果在創建 defaultdict 的時候沒有指定 default_factory,查詢不存在的鍵會觸發 KeyError。

defaultdict 裏的 default_factory 只會在__getitem__ 裏被調用,在其他的方法裏完全不會發揮作用。比如,dd 是個 defaultdict,k 是個找不到的鍵, dd[k] 這個表達式會調用 default_factory 創造某個默認值,而 dd.get(k) 則會返回 None。

所有這一切背後的功臣其實是特殊方法 __missing__。它會在defaultdict 遇到找不到的鍵的時候調用 default_factory,而實際上這個特性是所有映射類型都可以選擇去支持的。

特殊方法__missing__

所有的映射類型在處理找不到的鍵的時候,都會牽扯到 __missing__方法。這也是這個方法稱作“missing”的原因。雖然基類 dict 並沒有定義這個方法,但是 dict 是知道有這麽個東西存在的。也就是說,如果
有一個類繼承了 dict,然後這個繼承類提供了 __missing__ 方法,那麽在 __getitem__ 碰到找不到的鍵的時候,Python 就會自動調用它,而不是拋出一個 KeyError 異常。

_missing__ 方法只會被 __getitem__ 調用(比如在表達式 d[k] 中)。提供 __missing__ 方法對 get 或者__contains__(in 運算符會用到這個方法)這些方法的使用沒有影響。這也是我在上一節最後的警告中提到,defaultdict 中的default_factory 只對 __getitem__ 有作用的原因。

Tests for item retrieval using `d[key]` notation::
>>> d = StrKeyDict0([(2, two), (4, four)])
>>> d[2]
two
>>> d[4]
four
>>> d[1]
Traceback (most recent call last):
...
KeyError: 1
Tests for item retrieval using `d.get(key)` notation::
>>> d.get(2)
two
>>> d.get(4)
four
>>> d.get(1, N/A)
N/A
Tests for the `in` operator::
>>> 2 in d
True
>>> 1 in d
False

StrKeyDict0 在查詢的時候把非字符串的鍵轉換為字符串

class StrKeyDict0(dict): ?
    def __missing__(self, key):
        if isinstance(key, str): ?
            raise KeyError(key)
        return self[str(key)] ?
    def get(self, key, default=None):
        try:
            return self[key] ?
        except KeyError:
            return default ?
    def __contains__(self, key):
        return key in self.keys() or str(key) in self.keys() ?

? StrKeyDict0 繼承了 dict。
? 如果找不到的鍵本身就是字符串,那就拋出 KeyError 異常。
? 如果找不到的鍵不是字符串,那麽把它轉換成字符串再進行查找。
? get 方法把查找工作用 self[key] 的形式委托給 __getitem__,這樣在宣布查找失敗之前,還能通過 __missing__ 再給某個鍵一個機會。
? 如果拋出 KeyError,那麽說明 __missing__ 也失敗了,於是返回default。
? 先按照傳入鍵的原本的值來查找(我們的映射類型中可能含有非字符串的鍵),如果沒找到,再用 str() 方法把鍵轉換成字符串再查找一次。

如果沒有這個測試,只要 str(k) 返回的是一個存在的鍵,那麽__missing__ 方法是沒問題的,不管是字符串鍵還是非字符串鍵,它都能正常運行。但是如果 str(k) 不是一個存在的鍵,代碼就會陷入無
限遞歸。這是因為 __missing__ 的最後一行中的 self[str(key)] 會調用 __getitem__,而這個 str(key) 又不存在,於是 __missing__又會被調用。

為了保持一致性,__contains__ 方法在這裏也是必需的。這是因為 kin d 這個操作會調用它,但是我們從 dict 繼承到的 __contains__方法不會在找不到鍵的時候調用 __missing__ 方法。__contains__
裏還有個細節,就是我們這裏沒有用更具 Python 風格的方式——k in my_dict——來檢查鍵是否存在,因為那也會導致 __contains__ 被遞歸調用。為了避免這一情況,這裏采取了更顯式的方法,直接在這個
self.keys() 裏查詢。

像 k in my_dict.keys() 這種操作在 Python 3 中是很快的,而且即便映射類型對象很龐大也沒關系。這是因為dict.keys() 的返回值是一個“視圖”。視圖就像一個集合,而且跟字典類似的是,在視圖裏查找一個元素的速度很快。

不可變映射類型

標準庫裏所有的映射類型都是可變的,但有時候你會有這樣的需求,比如不能讓用戶錯誤地修改某個映射。

從 Python 3.3 開始,types 模塊中引入了一個封裝類名叫MappingProxyType。如果給這個類一個映射,它會返回一個只讀的映射視圖。雖然是個只讀視圖,但是它是動態的。這意味著如果對原映射
做出了改動,我們通過這個視圖可以觀察到,但是無法通過這個視圖對原映射做出修改。

用 MappingProxyType 來獲取字典的只讀實例mappingproxy

>>> from types import MappingProxyType
>>> d = {1:A}
>>> d_proxy = MappingProxyType(d)
>>> d_proxy
mappingproxy({1: A})
>>> d_proxy[1] ?
A
>>> d_proxy[2] = x ?
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: mappingproxy object does not support item assignment
>>> d[2] = B
>>> d_proxy ?
mappingproxy({1: A, 2: B})
>>> d_proxy[2]
B
>>>

? d 中的內容可以通過 d_proxy 看到。
? 但是通過 d_proxy 並不能做任何修改。
? d_proxy 是動態的,也就是說對 d 所做的任何改動都會反饋到它上面。

集合論

“集”這個概念在 Python 中算是比較年輕的,同時它的使用率也比較低。set 和它的不可變的姊妹類型 frozenset 直到 Python 2.3 才首次以模塊的形式出現,然後在 Python 2.6 中它們升級成為內置類型。

集合的本質是許多唯一對象的聚集。因此,集合可以用於去重:

>>> l = [spam, spam, eggs, spam]
>>> set(l)
{eggs, spam}
>>> list(set(l))
[eggs, spam]

集合中的元素必須是可散列的,set 類型本身是不可散列的,但是frozenset 可以。因此可以創建一個包含不同 frozenset 的 set。除了保證唯一性,集合還實現了很多基礎的中綴運算符。給定兩個集合
a 和 b,a | b 返回的是它們的合集,a & b 得到的是交集,而 a - b得到的是差集。合理地利用這些操作,不僅能夠讓代碼的行數變少,還能減少 Python 程序的運行時間。這樣做同時也是為了讓代碼更易讀,從
而更容易判斷程序的正確性,因為利用這些運算符可以省去不必要的循環和邏輯操作。

例如,我們有一個電子郵件地址的集合(haystack),還要維護一個
較小的電子郵件地址集合(needles),然後求出 needles 中有多少地
址同時也出現在了 heystack 裏。借助集合操作,我們只需要一行代碼
就可以了

needles 的元素在 haystack 裏出現的次數,兩個變量都是 set 類型

found = len(needles & haystack)

如果不使用交集操作的話,代碼可能就變成了

found = 0
for n in needles:
if n in haystack:
found += 1

使用集合的內置方法會比用循環速度快

不要忘了,如果要創建一個空集,你必須用不帶任何參數的構造方法 set()。如果只是寫成 {} 的形式,跟以前一樣,你創建的其實是個空字典。

>>> s = {1}
>>> type(s)
<class set>
>>> s
{1}
>>> s.pop()
1
>>> s
set()

集合的操作

列出了可變和不可變集合所擁有的方法的概況,其中不少是運算符重載的特殊方法。包含了數學裏集合的各種操作在 Python 中所對應的運算符和方法。
技術分享圖片

集合的數學運算

技術分享圖片

dict和set的背後

想要理解 Python 裏字典和集合類型的長處和弱點,它們背後的散列表是繞不開的一環。

  • Python 裏的 dict 和 set 的效率有多高?
  • 為什麽它們是無序的?
  • 為什麽並不是所有的 Python 對象都可以當作 dict 的鍵或 set 裏的元素?
  • 為什麽 dict 的鍵和 set 元素的順序是跟據它們被添加的次序而定的,以及為什麽在映射對象的生命周期中,這個順序並不是一成不變的
  • 為什麽不應該在叠代循環 dict 或是 set 的同時往裏添加元素?

一個關於效率的實驗

為了對比容器的大小對 dict、set 或 list 的 in 運算符效率的影響,我創建了一個有 1000 萬個雙精度浮點數的數組,名叫 haystack。另外還有一個包含了 1000 個浮點數的 needles 數組,其中 500 個數字是從
haystack 裏挑出來的,另外 500 個肯定不在 haystack 裏。作為 dict 測試的基準,我用 dict.fromkeys() 來建立了一個含有1000 個浮點數的名叫 haystack 的字典,並用 timeit 模塊測試示例 3-14(與示例 3-11 相同)裏這段代碼運行所需要的時間。

在 haystack 裏查找 needles 的元素,並計算找到的元素的個數

found = 0
for n in needles:
    if n in haystack:
        found += 1

技術分享圖片

也就是說,在從 1000 個字典鍵裏搜索 1000 個浮點數所需的時間是 0.000202 秒,把同樣的搜索在含有 10 000 000 個元素的字典裏進行一遍,只需要 0.000337 秒。換句話說,在一個有 1000 萬個鍵的
字典裏查找 1000 個數,花在每個數上的時間不過是 0.337 微秒——沒錯,相當於平均每個數差不多三分之一微秒。作為對比,我把 haystack 換成了 set 和 list 類型,重復了同樣的增長大小的實驗。對於 set,除了上面的那個循環的運行時間,我還測量了示例 3-15 那行代碼,這段代碼也計算了 needles 中出現在
haystack 中的元素的個數。

利用交集來計算 needles 中出現在 haystack 中的元素的個數

found = len(needles & haystack)

列出了所有測試的結果。

技術分享圖片

字典中的散列表

散列表其實是一個稀疏數組(總是有空白元素的數組稱為稀疏數組)。在一般的數據結構教材中,散列表裏的單元通常叫作表元(bucket)。在 dict 的散列表當中,每個鍵值對都占用一個表元,每個表元都有兩個部分,一個是對鍵的引用,另一個是對值的引用。因為所有表元的大小一致,所以可以通過偏移量來讀取某個表元。

因為 Python 會設法保證大概還有三分之一的表元是空的,所以在快要達
到這個閾值的時候,原有的散列表會被復制到一個更大的空間裏面。
如果要把一個對象放入散列表,那麽首先要計算這個元素鍵的散列值。Python 中可以用 hash() 方法來做這件事情,接下來會介紹這一點。


散列值和相等性

內置的 hash() 方法可以用於所有的內置類型對象。如果是自定義對象調用 hash() 的話,實際上運行的是自定義的 __hash__。如果兩個對象在比較的時候是相等的,那它們的散列值必須相等,否
則散列表就不能正常運行了。例如,如果 1 == 1.0 為真,那麽hash(1) == hash(1.0) 也必須為真,但其實這兩個數字(整型和浮點)的內部結構是完全不一樣的.

為了讓散列值能夠勝任散列表索引這一角色,它們必須在索引空間中盡量分散開來。這意味著在最理想的狀況下,越是相似但不相等的對象,它們散列值的差別應該越大。示例 3-16 是一段代碼輸
出,這段代碼被用來比較散列值的二進制表達的不同。註意其中 1和 1.0 的散列值是相同的,而 1.0001、1.0002 和 1.0003 的散列值則非常不同。

從 Python 3.3 開始,str、bytes 和 datetime 對象的散列值計算過程中多了隨機的“加鹽”這一步。所加鹽值是 Python進程內的一個常量,但是每次啟動 Python 解釋器都會生成一個不同的鹽值。隨機鹽值的加入是為了防止 DOS 攻擊而采取的一種安全措施。

散列表算法

為了獲取 my_dict[search_key] 背後的值,Python 首先會調用
hash(search_key) 來計算 search_key 的散列值,把這個值最低
的幾位數字當作偏移量,在散列表裏查找表元(具體取幾位,得看
當前散列表的大小)。若找到的表元是空的,則拋出 KeyError 異
常。若不是空的,則表元裏會有一對 found_key:found_value。
這時候 Python 會檢驗 search_key == found_key 是否為真,如
果它們相等的話,就會返回 found_value。


如果 search_key 和 found_key 不匹配的話,這種情況稱為散列
沖突。發生這種情況是因為,散列表所做的其實是把隨機的元素映
射到只有幾位的數字上,而散列表本身的索引又只依賴於這個數字
的一部分。為了解決散列沖突,算法會在散列值中另外再取幾位,
然後用特殊的方法處理一下,把新得到的數字再當作索引來尋找表
元。 若這次找到的表元是空的,則同樣拋出 KeyError;若非
空,或者鍵匹配,則返回這個值;或者又發現了散列沖突,則重復
以上的步驟。下圖展示了這個算法的示意圖。

技術分享圖片

添加新元素和更新現有鍵值的操作幾乎跟上面一樣。只不過對於前
者,在發現空表元的時候會放入一個新元素;對於後者,在找到相
對應的表元後,原表裏的值對象會被替換成新值。


另外在插入新值時,Python 可能會按照散列表的擁擠程度來決定是
否要重新分配內存為它擴容。如果增加了散列表的大小,那散列值
所占的位數和用作索引的位數都會隨之增加,這樣做的目的是為了
減少發生散列沖突的概率。


表面上看,這個算法似乎很費事,而實際上就算 dict 裏有數百萬
個元素,多數的搜索過程中並不會有沖突發生,平均下來每次搜索
可能會有一到兩次沖突。在正常情況下,就算是最不走運的鍵所遇
到的沖突的次數用一只手也能數過來。


了解 dict 的工作原理能讓我們知道它的所長和所短,以及從它衍
生而來的數據類型的優缺點。下面就來看看 dict 這些特點背後的
原因。

dict的實現及其導致的結果

使用散列表給 dict 帶來的優勢和限制都有哪些。

鍵必須是可散列的

(1) 支持 hash() 函數,並且通過 __hash__() 方法所得到的散列值是不變的。
(2) 支持通過 __eq__() 方法來檢測相等性。
(3) 若 a == b 為真,則 hash(a) == hash(b) 也為真。所有由用戶自定義的對象默認都是可散列的,因為它們的散列值由id() 來獲取,而且它們都是不相等的。

字典在內存上的開銷巨大

由於字典使用了散列表,而散列表又必須是稀疏的,這導致它在空
間上的效率低下。舉例而言,如果你需要存放數量巨大的記錄,那
麽放在由元組或是具名元組構成的列表中會是比較好的選擇;最好
不要根據 JSON 的風格,用由字典組成的列表來存放這些記錄。用
元組取代字典就能節省空間的原因有兩個:其一是避免了散列表所
耗費的空間,其二是無需把記錄中字段的名字在每個元素裏都存一
遍。


在用戶自定義的類型中,__slots__ 屬性可以改變實例屬性的存儲
方式,由 dict 變成 tuple。
記住我們現在討論的是空間優化。如果你手頭有幾百萬個對象,而
你的機器有幾個 GB 的內存,那麽空間的優化工作可以等到真正需
要的時候再開始計劃,因為優化往往是可維護性的對立面。

鍵查詢很快

dict 的實現是典型的空間換時間:字典類型有著巨大的內存開
銷,但它們提供了無視數據量大小的快速訪問——只要字典能被裝
在內存裏。正如表 3-5 所示,如果把字典的大小從 1000 個元素增
加到 10 000 000 個,查詢時間也不過是原來的 2.8 倍,從 0.000163
秒增加到了 0.00456 秒。這意味著在一個有 1000 萬個元素的字典
裏,每秒能進行 200 萬個鍵查詢。

鍵的次序取決於添加順序

當往 dict 裏添加新鍵而又發生散列沖突的時候,新鍵可能會被安
排存放到另一個位置。於是下面這種情況就會發生:由
dict([key1, value1), (key2, value2)] 和 dict([key2,
value2], [key1, value1]) 得到的兩個字典,在進行比較的時
候,它們是相等的;但是如果在 key1 和 key2 被添加到字典裏的
過程中有沖突發生的話,這兩個鍵出現在字典裏的順序是不一樣
的。

# 世界人口數量前10位國家的電話區號
DIAL_CODES = [
    (86, China),
    (91, India),
    (1, United States),
    (62, Indonesia),
    (55, Brazil),
    (92, Pakistan),
    (880, Bangladesh),
    (234, Nigeria),
    (7, Russia),
    (81, Japan),
]
d1 = dict(DIAL_CODES) ?
print(d1:, d1.keys())
d2 = dict(sorted(DIAL_CODES)) ?
print(d2:, d2.keys())
d3 = dict(sorted(DIAL_CODES, key=lambda x:x[1])) ?
print(d3:, d3.keys())
assert d1 == d2 and d2 == d3 ?

? 創建 d1 的時候,數據元組的順序是按照國家的人口排名來決定的。
? 創建 d2 的時候,數據元組的順序是按照國家的電話區號來決定的。
? 創建 d3 的時候,數據元組的順序是按照國家名字的英文拼寫來決定的。
? 這些字典是相等的,因為它們所包含的數據是一樣的。

往字典裏添加新鍵可能會改變已有鍵的順序

無論何時往字典裏添加新的鍵,Python 解釋器都可能做出為字典擴
容的決定。擴容導致的結果就是要新建一個更大的散列表,並把字
典裏已有的元素添加到新表裏。這個過程中可能會發生新的散列沖
突,導致新散列表中鍵的次序變化。要註意的是,上面提到的這些
變化是否會發生以及如何發生,都依賴於字典背後的具體實現,因
此你不能很自信地說自己知道背後發生了什麽。如果你在叠代一個
字典的所有鍵的過程中同時對字典進行修改,那麽這個循環很有可
能會跳過一些鍵——甚至是跳過那些字典中已經有的鍵。


由此可知,不要對字典同時進行叠代和修改。如果想掃描並修改一
個字典,最好分成兩步來進行:首先對字典叠代,以得出需要添加
的內容,把這些內容放在一個新字典裏;叠代結束之後再對原有字
典進行更新。

set的實現以及導致的結果

set 和 frozenset 的實現也依賴散列表,但在它們的散列表裏存放的
只有元素的引用(就像在字典裏只存放鍵而沒有相應的值)。在 set 加
入到 Python 之前,我們都是把字典加上無意義的值當作集合來用的。
在 節中所提到的字典和散列表的幾個特點,對集合來說幾乎都是
適用的。為了避免太多重復的內容,這些特點總結如下。

  • 集合裏的元素必須是可散列的。
  • 集合很消耗內存。
  • 可以很高效地判斷元素是否存在於某個集合。
  • 元素的次序取決於被添加到集合裏的次序。
  • 往集合裏添加元素,可能會改變集合裏已有元素的次序。

流暢的python 第三張 字典和集合