這一篇是
《流暢的 python》
讀書筆記。主要介紹:html
- 常見的字典方法
- 如何處理查不到的鍵
- 標準庫中 dict 類型的變種
- 散列表的工做原理
collections.abc 模塊中有 Mapping 和 MutableMapping 這兩個抽象基類,它們的做用是爲 dict 和其餘相似的類型定義形式接口。python
標準庫裏全部映射類型都是利用 dict 來實現的,它們有個共同的限制,即只有可散列的數據類型才能用作這些映射裏的鍵。算法
問題:
什麼是可散列的數據類型?數組
在 python 詞彙表(docs.python.org/3/glossary.…)中,關於可散列類型的定義是這樣的:app
若是一個對象是可散列的,那麼在這個對象的生命週期中,它的散列值是不變的,並且這個對象須要實現
__hash__()
方法。另外可散列對象還要有__eq__()
方法,這樣才能跟其餘鍵作比較。若是兩個可散列對象是相等的,那麼它們的散列只必定是同樣的函數
根據這個定義,原子不可變類型(str,bytes和數值類型)都是可散列類型,frozenset 也是可散列的(由於根據其定義,frozenset 裏只能容納可散列類型),若是元組內都是可散列類型的話,元組也是可散列的(元組雖然是不可變類型,但若是它裏面的元素是可變類型,這種元組也不能被認爲是不可變的)。ui
通常來說,用戶自定義的類型的對象都是可散列的,散列值就是它們的 id() 函數的返回值,因此這些對象在比較的時候都是不相等的。(若是一個對象實現了 eq 方法,而且在方法中用到了這個對象的內部狀態的話,那麼只有當全部這些內部狀態都是不可變的狀況下,這個對象纔是可散列的。)spa
根據這些定義,字典提供了不少種構造方法,docs.python.org/3/library/s… 這個頁面有個例子來講明建立字典的不一樣方式。code
>>> 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
複製代碼
除了這些方法之外,還能夠用字典推導的方式來建造新 dict。cdn
自 Python2.7 以來,列表推導和生成器表達式的概念就移植到了字典上,從而有了字典推導。字典推導(dictcomp)能夠從任何以鍵值對做爲元素的可迭代對象中構建出字典。
好比:
>>> data = [(1, 'a'), (2, 'b'), (3, 'c')]
>>> data_dict = {num: letter for num, letter in data}
>>> data_dict
{1: 'a', 2: 'b', 3: 'c'}
複製代碼
下表爲咱們展現了 dict、defaultdict 和 OrderedDict 的常見方法(後兩種是 dict 的變種,位於 collections模塊內)。
default_factory 並非一個方法,而是一個可調用對象,它的值 defaultdict 初始化的時候由用戶設定。
OrderedDict.popitem() 會移除字典最早插入的元素(先進先出);可選參數 last 若是值爲真,則會移除最後插入的元素(後進先出)。
用 setdefault 處理找不到的鍵 當字典 d[k] 不能找到正確的鍵的時候,Python 會拋出異常,平時咱們都使用d.get(k, default)
來代替 d[k],給找不到的鍵一個默認值,還可使用效率更高的 setdefault
my_dict.setdefault(key, []).append(new_value)
# 等同於
if key not in my_dict:
my_dict[key] = []
my_dict[key].append(new_value)
複製代碼
這兩段代碼的效果同樣,只不過,後者至少要進行兩次鍵查詢,若是不存在,就是三次,而用 setdefault
只需一次就能夠完成整個操做。
那麼,咱們取值的時候,該如何處理找不到的鍵呢?
有時候,就算某個鍵在映射裏不存在,咱們也但願在經過這個鍵讀取值的時候能獲得一個默認值。有兩個途徑能幫咱們達到這個目的,
一個是經過 defaultdict
這個類型而不是普通的 dict,另外一個是給本身定義一個 dict
的子類,而後在子類中實現__missing__
方法。
首先咱們看下如何使用 defaultdict :
import collections
index = collections.defaultdict(list)
index[new_key].append(new_value)
複製代碼
這裏咱們新建了一個字典 index,若是鍵 new_key
在 index 中不存在,表達式 index[new_key]
會按如下步驟來操做:
而這個用來生成默認值的可調用對象存放在名爲 default_factory
的實例屬性中。
defaultdict 中的 default_factory 只會在 getitem 裏調用,在其餘方法中不會發生做用。好比 index[k] 這個表達式會調用 default_factory 創造的某個默認值,而 index.get(k) 則會返回 None。(這是由於特殊方法 missing 會在 defaultdict 遇到找不到的鍵的時候調用 default_factory,實際上,這個特性全部映射方法均可以支持)。
全部映射在處理找不到的鍵的時候,都會牽扯到 missing 方法。但基類 dict 並無提供 這個方法。不過,若是有一個類繼承了 dict ,而後這個繼承類提供了 missing 方法,那麼在 getitem 碰到找不到鍵的時候,Python 會自動調用它,而不是拋出一個 KeyError 異常。
__missing__
方法只會被__getitem__
調用。提供 missing 方法對 get 或者 contains(in 運算符會用到這個方法)這些方法的是有沒有影響。
下面這段代碼實現了 StrKeyDict0 類,StrKeyDict0 類在查詢的時候把非字符串的鍵轉化爲字符串。
class StrKeyDict0(dict): # 繼承 dict
def __missing__(self, key):
if isinstance(key, str):
# 若是找不到的鍵自己就是字符串,拋出 KeyError
raise KeyError(key)
# 若是找不到的鍵不是字符串,轉化爲字符串再找一次
return self[str(key)]
def get(self, key, default=None):
# get 方法把查找工做用 self[key] 的形式委託給 __getitem__,這樣在宣佈查找失敗錢,還能經過 __missing__ 再給鍵一個機會
try:
return self[key]
except KeyError:
# 若是拋出 KeyError 說明 __missing__ 也失敗了,因而返回 default
return default
def __contains__(self, key):
# 先按傳入的鍵查找,若是沒有再把鍵轉爲字符串再找一次
return key in self.keys() or str(key) in self.keys()
複製代碼
contains 方法存在是爲了保持一致性,由於 k in d 這個操做會調用它,但咱們從 dict 繼承到的 contains 方法不會在找不到鍵的時候用 missing 方法。
my_dict.keys() 在 Python3 中返回值是一個 "視圖","視圖"就像是一個集合,並且和字典同樣速度很快。但在 Python2中,my_dict.keys() 返回的是一個列表。 因此 k in my_dict.keys() 操做在 python3中速度很快,但在 python2 中,處理效率並不高。
若是要自定義一個映射類型,合適的策略是繼承
collections.UserDict
類。這個類就是把標準 dict 用 python 又實現了一遍,UserDict 是讓用戶繼承寫子類的,改進後的代碼以下:
import collections
class StrKeyDict(collections.UserDict):
def __missing__(self, key):
if isinstance(key, str):
raise KeyError(key)
return self[str(key)]
def __contains__(self, key):
# 這裏能夠放心假設全部已經存儲的鍵都是字符串。所以只要在 self.data 上查詢就行了
return str(key) in self.data
def __setitem__(self, key, item):
# 這個方法會把全部的鍵都轉化成字符串。
self.data[str(key)] = item
複製代碼
由於 UserDict 繼承的是 MutableMapping,因此 StrKeyDict 裏剩下的那些映射類型都是從 UserDict、MutableMapping 和 Mapping 這些超類繼承而來的。
Mapping 中提供了 get 方法,和咱們在 StrKeyDict0 中定義的同樣,因此咱們在這裏不須要定義 get 方法。
在 collections 模塊中,除了 defaultdict 以外還有其餘的映射類型。
問題:
標準庫中全部的映射類型都是可變的,若是咱們想給用戶提供一個不可變的映射類型該如何處理呢?
從 Python3.3 開始 types 模塊中引入了一個封裝類名叫 MappingProxyType
。若是給這個類一個映射,它會返回一個只讀的映射視圖(若是原映射作了改動,這個視圖的結果頁會相應的改變)。例如
>>> from types import MappingProxy Type
>>> 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[2] # d_proxy 是動態的,d 的改動會反饋到它上邊
'B'
複製代碼
散列表實際上是一個稀疏數組(總有空白元素的數組叫稀疏數組),在 dict 的散列表中,每一個鍵值都佔用一個表元,每一個表元都有兩個部分,一個是對鍵的引用,另外一個是對值的引用
。由於全部表元的大小一致,因此能夠經過偏移量來讀取某個表元
。 python 會設法保證大概有1/3 的表元是空的,因此在快要達到這個閾值的時候,原有的散列表會被複制到一個更大的空間。
若是要把一個對象放入散列表,那麼首先要計算這個元素的散列值。 Python內置的 hash() 方法能夠用於計算全部的內置類型對象。
若是兩個對象在比較的時候是相等的,那麼它們的散列值也必須相等。例如 1==1.0 那麼,hash(1) == hash(1.0)
爲了獲取 my_dict[search_key] 的值,Python 會首先調用 hash(search_key) 來計算 search_key 的散列值,把這個值的最低幾位當作偏移量在散列表中查找元。若表元爲空,拋出 KeyError 異常。若不爲空,則表元會有一對 found_key:found_value
。 這時須要校驗 search_key == found_key,若是相等,返回 found_value。 若是不匹配(散列衝突),再在散列表中再取幾位,而後處理一下,用處理後的結果當作索引再找表元。 而後重複上面的步驟。
取值流程圖以下:
添加新值和上述的流程基本一致,只不過對於前者,在發現空表元的時候會放入一個新元素,而對於後者,在找到相應表元后,原表裏的值對象會被替換成新值。
另外,在插入新值是,Python 可能會按照散列表的擁擠程度來決定是否從新分配內存爲它擴容,
若是增長了散列表的大小,那散列值所佔的位數和用做索引的位數都會隨之增長
可散列對象要求以下:
由於字典使用了散列表,而散列表又必須是稀疏的,這致使它在空間上效率低下。
dict 的實現是典型的空間換時間:字典類型由着巨大的內存開銷,但提供了無視數據量大小的快速訪問。
當往 dict 裏添加新鍵而又發生散列衝突時,新建可能會被安排存放在另外一個位置。
不管什麼時候向字典中添加新的鍵,Python 解釋器均可能作出爲字典擴容的決定。擴容致使的結果就是要新建一個更大的散列表,並把原有的鍵添加到新的散列表中,這個過程當中可能會發生新的散列衝突,致使新散列表中次序發生變化。 所以,不要對字典同時進行迭代和修改。
這一篇主要介紹了:
最後,感謝女友支持。
歡迎關注(April_Louisa) | 請我喝芬達 |
---|---|
![]() |
![]() |