字典是內(nèi)置類型中唯一的映射(Mapping)結(jié)構(gòu)矛缨,基于哈希表存儲鍵值對數(shù)據(jù)而昨。
值可以是任意類型的數(shù)據(jù)帆焕,但主鍵必須是可哈希的類型惭婿。常見的可變類型,如列表叶雹、集合等都不能作為主鍵使用财饥。即便是元組等不可變類型,也不能引用可變類型元素折晦,即元組中不能含有可變類型的元素钥星。
>>> import collections
>>> issubclass(list, collections.Hashable)
False
>>> issubclass(int, collections.Hashable)
True
>>> hash((1, 2, 3))
2528502973977326415
>>> hash((1, 2, [1, 2])) # 包含可變類型
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
哈希計(jì)算通過調(diào)用 __hash__ 方法返回整數(shù)值,用來快速比較內(nèi)容是否相同满着。某些類型雖然有該方法谦炒,但實(shí)際無法執(zhí)行,故不能作為主鍵使用风喇。另外宁改,主鍵對象的哈希值必須恒定不變,否則無法查找鍵值魂莫,甚至?xí)l(fā)錯誤还蹲。
>>> callable(list().__hash__)
False
自定義類型默認(rèn)實(shí)現(xiàn)了 __hash__ 和 __eq__ 方法,用與哈希和相等比較操作耙考。前者為每個實(shí)例返回隨機(jī)值谜喊;后者除非與自己比較,否則總是返回 False琳骡。這兩個方法可根據(jù)需要進(jìn)行重載锅论。
作為常用的數(shù)據(jù)結(jié)構(gòu),又因?yàn)槊臻g的緣故楣号,字典的使用頻率非常高最易。Python 開發(fā)團(tuán)隊(duì)也一直致力于改進(jìn)其數(shù)據(jù)結(jié)構(gòu)和算法怒坯,這其中自然也包括慣用的緩存復(fù)用。
Python 3.6 借鑒 PyPy 字典設(shè)計(jì)藻懒,采用更緊湊的存儲結(jié)構(gòu)剔猿。keys.entries 和 values 用數(shù)組按添加順序存儲主鍵和值引用。實(shí)際哈希表由 keys.indices 數(shù)組承擔(dān)嬉荆,通過計(jì)算主鍵哈希值找到合適的位置归敬,然后在該位置存儲主鍵在 key.entries 的實(shí)際索引。如此一來鄙早,只要通過 indices 獲取實(shí)際索引后汪茧,就可讀取主鍵和值信息了。
雖然該版本按添加順序存儲元素限番,但內(nèi)部實(shí)現(xiàn)不能作為依賴條件舱污。在后續(xù)版本中,可能有其他變化弥虐。如有明確順序需求扩灯,建議使用 collections.OrderDict。
系統(tǒng)分別緩存復(fù)用 80 個 dict 和 keys霜瘪,其中包括長度為 8 的 entries 內(nèi)存珠插。對于大量小字典對象而言,直接使用颖对,無須任何內(nèi)存分配操作捻撑。回收時惜互,凡內(nèi)存被擴(kuò)張過的都會被放棄布讹。
從開發(fā)地址法(open-address)實(shí)現(xiàn)方式來看,它并不適合處理大數(shù)據(jù)训堆。輕量級方案可選用 shelve描验、dbm 等標(biāo)準(zhǔn)庫模塊,也可直接采用 SQLite坑鱼、LevelDB 等專業(yè)數(shù)據(jù)庫膘流。
構(gòu)建
創(chuàng)建字典對象可以使用大括號鍵值對方式創(chuàng)建,或調(diào)用類型構(gòu)造鲁沥。
>>> {"a": 1, "b": 2}
{'a': 1, 'b': 2}
>>> dict(a=1, b=2)
{'a': 1, 'b': 2}
初始化鍵值參數(shù)也可以用元組呼股、列表等可迭代對象的方式提供。
>>> kvs = (("a", 1), ["b", 2])
>>> dict(kvs)
{'a': 1, 'b': 2}
基于動態(tài)數(shù)據(jù)創(chuàng)建時画恰,多以 zip彭谁、map 函數(shù)或推導(dǎo)式完成。
>>> dict(zip("abc", range(3)))
{'a': 0, 'b': 1, 'c': 2}
>>> dict(map(lambda k, v: (k, v + 10), "abc", range(3)))
{'a': 10, 'b': 11, 'c': 12}
>>> {k: v + 10 for k, v in zip("abc", range(3))}
{'a': 10, 'b': 11, 'c': 12}
除了直接提供內(nèi)容外允扇,某些時候缠局,還須根據(jù)一定條件初始化字典對象则奥。比如,基于已有字典內(nèi)容擴(kuò)展狭园,或初始化零值等读处。
>>> a = {"a": 1}
>>> b = dict(a, b=1) # 在復(fù)制 a 內(nèi)容的基礎(chǔ)上,新增鍵值對
>>> b
{'a': 1, 'b': 1}
>>> c = dict.fromkeys(b, 0) # 僅用 b 的主鍵唱矛,內(nèi)容另設(shè)
>>> c
{'a': 0, 'b': 0}
>>> d = dict.fromkeys(("counter1", "counter2"), 0) # 顯示提供主鍵
>>> d
{'counter1': 0, 'counter2': 0}
相比于 fromkeys 方法罚舱,推導(dǎo)式可完成更復(fù)雜的操作,比如額外的 if 過濾條件绎谦。
操作
字典不是序列類型管闷,不支持序號訪問,可以使用主鍵(鍵值)讀取燥滑、新增或刪除內(nèi)容渐北。
若主鍵(鍵值)不存在,會引發(fā) KeyError 異常铭拧,可以先用 in、not in 語句判斷是否存在該主鍵(鍵值)恃锉,或用 get 方法返回默認(rèn)值搀菩。
get 方法默認(rèn)值參數(shù)僅返回,不影響字典內(nèi)容破托。但某些時候肪跋,我們還須向字典插入默認(rèn)值,比如用字典存儲多個計(jì)數(shù)器土砂,那么在第一次取值時延遲初始化很有必要州既。在字典內(nèi)有零值內(nèi)容代表該計(jì)數(shù)曾被使用,沒有則無法記錄該行為萝映。
>>> x = {}
>>> x.setdefault("a", 0) # 如果有 a吴叶,那么返回實(shí)際內(nèi)容,否則新增{a:0}鍵值對
0
>>> x
{'a': 0}
>>> x["a"] = 100
>>> x.setdefault("a", 0)
100
字典不支持加法序臂、乘法蚌卤、大小等運(yùn)算,但可比較內(nèi)容是否相同奥秆。
>>> {"a": 1, "b": 2} == {"a": 1, "b": 2}
True
視圖
與早期版本復(fù)制數(shù)據(jù)并返回列表不同逊彭,Python 3 默認(rèn)以視圖關(guān)聯(lián)字典內(nèi)容。如此一來构订,既能避免復(fù)制開銷侮叮,還能同步觀察字典變化。
>>> x = dict(a = 1, b = 2)
>>> ks = x.keys() # 主鍵視圖
>>> for k in ks: print(k, x[k]) # 利用視圖迭代字典
a 1
b 2
<u>::這一段代碼不是很明白悼瘾,迭代獲取值還是從原來的字典中獲取的囊榜,為什么會叫視圖呢谷异?::</u>
字典沒有獨(dú)立的只讀版本,無論傳遞引用還是復(fù)制品锦聊,都存在弊端:
- 直接引用有被接收方修改內(nèi)容的風(fēng)險(xiǎn)
- 復(fù)制品僅是一次快照歹嘹,無法獲知字典的變化
視圖則不同,它能同步讀取字典內(nèi)容孔庭,卻無法修改尺上。且可選擇不同粒度的內(nèi)容進(jìn)行傳遞,如此可將接收方限定為指定模式下的觀察員圆到。
def test(d): # 傳遞鍵值視圖(items)怎抛,只能讀取,無法修改
for k, v in d:
print(k, v)
視圖還支持集合操作芽淡,以彌補(bǔ)字典功能上的不足马绝。
>>> a = dict(a = 1, b = 2)
>>> b = dict(c = 3, b = 2)
>>> ka = a.keys()
>>> kb = b.keys()
>>> ka & kb # 交集:在 a、b 中同時存在
{'b'}
>>> ka | kb # 并集:在 a 或 b 中存在
{'b', 'a', 'c'}
>>> ka - kb # 差集:僅在 a 中存在
{'a'}
>>> ka ^ kb # 對稱差集:僅在 a 或僅在 b 中出現(xiàn)挣菲,相當(dāng)于“并集-交集”
{'a', 'c'}
利用視圖的集合運(yùn)算富稻,可簡化某些操作。例如白胀,只更新椭赋,不新增。
>>> a = dict(a = 1, b = 2)
>>> b = dict(b = 20, c = 3)
>>> ks = a.keys() & b.keys() # 交集或杠,也就是 a 中必須存在的主鍵
>>> a.update({k: b[k] for k in ks}) # 利用交集結(jié)果提取待更新的內(nèi)容
>>> a
{'a': 1, 'b': 20}
拓展
在標(biāo)準(zhǔn)庫中哪怔,還有幾個擴(kuò)展類型的字典可供使用。
默認(rèn)字典(defaultdict)類似于 setdefault 方法的包裝向抢。當(dāng)主鍵不存在時认境,調(diào)用構(gòu)造參數(shù)提供的工廠函數(shù)返回默認(rèn)值。
將字典直接作為對外接口時挟鸠,無法保證用戶是否會調(diào)用 setdefault 或 get 方法叉信。這樣,默認(rèn)字典的內(nèi)置初始化行為就好于對用戶做額外要求兄猩。
>>> import collections
>>> d = collections.defaultdict(lambda : 100)
>>> d["a"]
100
>>> d["b"] += 1
>>> d
defaultdict(<function <lambda> at 0x10bfb4f28>, {'a': 100, 'b': 101})
與內(nèi)部實(shí)現(xiàn)無關(guān)茉盏,有序字典(OrderedDict)明確記錄主鍵首次插入的次序。
任何時候都要避免依賴內(nèi)部實(shí)現(xiàn)枢冤,或者說遵循“顯式優(yōu)于隱式”的規(guī)則鸠姨。
>>> d = collections.OrderedDict()
>>> d["z"] = 1
>>> d["a"] = 2
>>> d["x"] = 3
>>> for k, v in d.items(): print(k, v)
z 1
a 2
x 3
與前面所說不同,計(jì)數(shù)器(Counter)對于不存在的主鍵返回零,但不會新增,即將主鍵添加到字典中上祈。
可通過繼承并重載 __miss__ 方法新增鍵值
>>> d = collections.Counter()
>>> d["a"]
0
>>> d["b"] += 1
>>> d
Counter({'b': 1})
鏈?zhǔn)阶值洌–hainMap)以單一接口訪問多個字典內(nèi)容靠胜,其自身并不存儲數(shù)據(jù)。讀操作按參數(shù)順序依次查找各字典呻右,但修改操作(新增傲隶、更新英上、刪除)僅針對第一字典祟峦。
>>> a = dict(a = 1, b =2)
>>> a = dict(a = 1, b = 2)
>>> b = dict(b = 20, c = 30)
>>> x = collections.ChainMap(a, b)
>>> x["b"], x["c"] # 按順序命中
(2, 30)
>>> for k, v in x.items(): print(k, v) # 遍歷所有字典
b 2
a 1
c 30
>>> x["b"] = 999 # 更新罚斗,命中第一字典
>>> x["z"] = 888 # 新增,命中第一字典
>>> x
ChainMap({'a': 1, 'b': 999, 'z': 888}, {'b': 20, 'c': 30})
可利用鏈?zhǔn)阶值湓O(shè)計(jì)多層次的上下文(context)結(jié)構(gòu)宅楞。
合理的上下文類型针姿,須具備兩個基本特征。首先是繼承厌衙,所有設(shè)置可被調(diào)用鏈的后續(xù)函數(shù)讀染嘁;其次是修改僅針對當(dāng)前和后續(xù)邏輯婶希,不應(yīng)向無關(guān)的父級傳遞榕暇。如此,鏈?zhǔn)阶值洳檎掖涡虮旧砭褪抢^承的體現(xiàn)喻杈,而修改操作被限制在當(dāng)前第一字典中中彤枢,自然也不會影響父級字典的同名主鍵設(shè)置。