相信每個在 Python 使用 dict 的時候都有遇過:對 dict 查詢了沒有出現過的 key
,就產生了 KeyError
造成程式錯誤。
# 範例:計算字串內每個字母出現次數 >>> my_string = "aabbbbc" >>> char_count = {} >>> for s in my_string: ... if s not in char_count.keys(): ... char_count[s] = 0 ... char_count[s] += 1 >>> char_count {'a': 2, 'b': 4, 'c': 1} >>> char_count['g'] Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 'g'
上述計算字母出現次數的範例,看起來簡單,卻有個小問題:就算 dict 找不到 key
邏輯也很簡單—就是計數為 0!因此,範例中的寫法稍嫌麻煩:
- 建立 dict 時,要不斷用
if
檢查該key
有沒有出現過 - dict 建立完成後,要是查詢到沒出現過的
key
會出現KeyError
- 但是,沒出現過的字母不是 Error,而是出現 0 次
這些麻煩,使用 dict 的預設值就可以解決!Python 的 dict 有多種方法可以設定預設值,這則筆記將與各位分享其中 3 種使用 dict 查詢不到 key 時、採用預設值的方法:
get()
函式setdefault()
函式defaultdict
物件
我會用上述計算字母出現次數範例介紹該如何活用這些 dict 預設值技巧,也會比較三種方法的功能與效能、告訴你何時該用哪一招。相信讀者學會這些 dict 操作,Python code 寫起來會更流暢!
你是進階級的 Python 使用者嗎?筆者推薦你直接來嘗試本文最後的 挑戰題,看看自己是否各種方法都熟悉。
目錄
dict.get()
函式
get()
同樣是用來查詢 dict,它與 dict[key]
的差別是:
get()
不會出現KeyError
get(key[, default])
是 Python dict 物件的內建函式:
- 如果 dict 內
key
存在,回傳查詢到的 value - 如果 dict 內
key
不存在,回傳default
設置的值 default
是用來設定預設值的可選引數,若未指定則為None
範例改寫
# 範例:計算字串內每個字母出現次數 # 使用 dict.get(key, 0) # -> 當 key 找不到就回傳 0 >>> my_string = "aabbbbc" >>> char_count = {} >>> for s in my_string: ... char_count[s] = char_count.get(s, 0) + 1 >>> char_count {'a': 2, 'b': 4, 'c': 1} ## 設定預設值為 0,查詢沒出現過的字母,也不會 KeyError >>> char_count.get('g', 0) 0 ## 沒設定預設值就回傳 None 囉 >>> char_count.get('g')
dict.setdefault()
函式
setdefault()
與 get()
的作用幾乎相同、請參考上方 get()
說明,setdefault()
只多了一項功能:
dict 內
key
不存在時,setdefault()
不只回傳default
,還會更改 dict、將該key
的值設定為default
範例改寫
# 範例:計算字串內每個字母出現次數 # 使用 dict.setdefault(key, 0) # -> 當 key 找不到就回傳 0 # -> 並且更新 dict 內容 >>> my_string = "aabbbbc" >>> char_count = {} >>> for s in my_string: ... # 若找不到 key,則修改 dict ... char_count.setdefault(s, 0) ... # 因為先執行 setdefault ... # 所以 dict[key] 必不會出現 KeyError ... char_count[s] = char_count[s] + 1 >>> char_count {'a': 2, 'b': 4, 'c': 1} # setdefault 不只回傳預設值、還會更改 dict 內容 >>> char_count.setdefault('g', 42) 42 >>> char_count {'a': 2, 'b': 4, 'c': 1, 'g': 42}
collections.defaultdict
物件
defaultdict
是 Python 內建 library collections
內的類別之一,它是繼承自 dict 的子類別。
建構 defaultdict
物件時,需要傳入 default_factory
引數、用來宣告「怎麼設定預設值」,特別的是,此引數需要是一個可被呼叫物件(callable,大多時候就是 function 物件)。
而 defaultdict
的行為與 setdefault()
相似:
若是以
defaultdict[key]
查詢不存在的key
不只會回傳預設值、還會修改defaultdict
的內容
用文字說明有點抽象,直接用程式碼學習 defaultdict
宣告預設值的方式:
>>> from collections import defaultdict # 使用 list 預設值 >>> d = defaultdict(list) >>> d defaultdict(<class 'list'>, {}) >>> d["hello"] [] >>> d defaultdict(<class 'list'>, {'hello': []}) # 使用 string 常數預設值 # 注意引數要是 callable,所以使用 lambda >>> d = defaultdict(lambda: "我是預設值") >>> d["hello"] '我是預設值' # 錯誤示範:引數不是 callable >>> d = defaultdict("我是預設值") Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: first argument must be callable or None # 如果沒設置 default_factory 引數 # 還是會 KeyError >>> d = defaultdict() >>> d["hello"] Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 'hello'
範例改寫
# 範例:計算字串內每個字母出現次數 # 使用 collections.defaultdict # -> 當 key 找不到就回傳 0 >>> from collections import defaultdict >>> my_string = "aabbbbc" >>> char_count = defaultdict(int) >>> ## 等同於:defaultdict(lambda: 0) >>> for s in my_string: ... char_count[s] = char_count[s] + 1 >>> char_count defaultdict(<class 'int'>, {'a': 2, 'b': 4, 'c': 1}) >>> dict(char_count) {'a': 2, 'b': 4, 'c': 1} # 注意: # 就算是不存在的 key # 在 defaultdict 只要查詢過 # 就會更新 defaultdict 物件本身內容 >>> char_count['g'] 0 >>> char_count defaultdict(<class 'int'>, {'a': 2, 'b': 4, 'c': 1, 'g': 0})
三種方法比較
get()
、setdefault()
、defaultdict
,何時該用哪一個?我們可以依照使用情境,考慮功能與效能兩個面向決定。
功能差別
乍看之下三個方法都在設置預設值,實際上有個關鍵性的 使用情境差異:
建構完 dict 之後,再次用 dict[key] 查詢不存在的 key
時,你希望程式如何反應?KeyError
或是預設值?
- 查詢不存在的
key
產生KeyError
:使用get()
、setdefault()
建立 dict - 查詢不存在的
key
,回傳並設置預設值:建立defaultdict
物件
# 1. char_count = {} ... >>> char_count['???'] Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: '???' # 2. char_count = defaultdict(int) ... >>> char_count['???'] 0
效能差別
效能差異我們用 timeit.timeit()
函式直接測試,觀察這 3 種方法在 dict 查找 key
的花費時間差別:(以下用 i5 的 Macbook Pro 測試)
>>> def test_dict_access(d): ... for i in range(100): ... d[i] >>> def test_get(d): ... for i in range(100): ... d.get(i, -1) >>> def test_setdefault(d): ... for i in range(100): ... d.setdefault(i, -1) >>> def test_defaultdict(d): ... for i in range(100): ... d[i] >>> import timeit >>> print(timeit.timeit( ... "test_dict_access({i:i for i in range(100)})", ... "from __main__ import test_dict_access")) 11.30299491599726 >>> print(timeit.timeit( ... "test_get({i:i for i in range(100)})", ... "from __main__ import test_get")) 16.070979757001624 >>> print(timeit.timeit( ... "test_setdefault({i:i for i in range(100)})", ... "from __main__ import test_setdefault")) 17.516931142999965 >>> print(timeit.timeit( ... "test_defaultdict(defaultdict(lambda: -1, {i:i for i in range(100)}))", ... "from __main__ import test_defaultdict; from collections import defaultdict")) 14.216421057000844
設定 dict 預設值之後,速度都會比不設定預設值的單純 dict[key]
慢。而比起 get()
與 setdefault()
預設值是每次查找 key
時都要設定,defaultdict
只在物件宣告時設定一次預設值而已,所以 defaultdict
速度比其他兩個方法快一點。筆者好豪也在 StackOverFlow 上看到有網友使用 dis
函式庫,用反組譯碼來分析速度差異,有興趣的讀者可以參考 這篇問答。
總言之,是否要為 dict 的查找設定預設值,效能與功能的權衡要綜合考慮。
挑戰題
動筆寫出這篇筆記的契機、也是下列挑戰題的來源,是先前在 Python Taiwan Facebook 社群 分享 zip()
技巧 的貼文,當時網友提出的問題如下,有興趣的讀者可以先自行挑戰,看看用這篇筆記介紹的 dict 預設值方法可以怎麼解題:
請問以下陣列(list of lists) [ ['a', 1],['a', 2], ['b', 1], ['c', 1],['c', 6],['c', 8] ] 該怎麼轉換成 { 'a':[1, 2], 'b':[1], 'c':[1, 6, 8] }
以下我將分享 5 種解題方式。
### 以下範例程式碼,arr 都是同樣內容 >>> arr [['a', 1], ['a', 2], ['b', 1], ['c', 1], ['c', 6], ['c', 8]]
dict
的 get()
>>> d = {} >>> for k, v in arr: ... l = d.get(k, []) ... l.append(v) ... d[k] = l >>> d {'a': [1, 2], 'b': [1], 'c': [1, 6, 8]}
dict
的 setdefault()
>>> d = {} >>> for k, v in arr: ... d.setdefault(k, []).append(v) >>> d {'a': [1, 2], 'b': [1], 'c': [1, 6, 8]}
collections
的 defaultdict
>>> from collections import defaultdict >>> d = defaultdict(list) >>> for k, v in arr: ... d[k].append(v) >>> d defaultdict(<class 'list'>, {'a': [1, 2], 'b': [1], 'c': [1, 6, 8]}) >>> dict(d) {'a': [1, 2], 'b': [1], 'c': [1, 6, 8]}
pandas
的 groupby()
>>> import pandas as pd >>> df = pd.DataFrame(arr, columns=['key', 'value']) >>> df.groupby('key')['value'].apply(list).to_dict() {'a': [1, 2], 'b': [1], 'c': [1, 6, 8]}
itertools
的 groupby()
>>> from itertools import groupby >>> grp_obj = groupby(arr, lambda x: x[0]) >>> {k: [x[1] for x in grp] for k, grp in grp_obj} {'a': [1, 2], 'b': [1], 'c': [1, 6, 8]}
同樣一個任務,有那麼多種做法,你是否跟我一樣更覺得 Python 有趣了呢!
結語
Python 為 dict 查找 key
行為設置預設值的方式有許多種,要依照使用情況所需功能判斷該用哪個。從功能面來看,「預設值是否需要隨情況改變」以及「後續查找是否要讓 dict 出現 KeyError
」,我們可以決定到底要用 get()
、setdefault()
、還是 defaultdict
。
不過,get()
設定預設值後雖然讓你查找 key
永遠免於面對 KeyError
,卻會讓你的查找 key
效能遜於單純的 dict[key]
。
究竟需不需要為 dict 查找 key
設置預設值,以及該用哪種方法來設置預設值,相信各位詳讀並練習這篇教學之後就會更加熟練了!
參考資料
學了 dict 的預設值設定,你知道 Python 函式設定預設值時也有些小陷阱嗎?推薦你閱讀好豪的另一篇筆記:Python 函式的可變物件預設引數陷阱,繼續學習!
此外,如果你想一次學會更多 Python 技巧,也推薦你閱讀好豪蒐集的《Python 神乎其技》免費教學文章,幫助你寫出 Pythonic Code!
如果你喜歡這篇教學,歡迎追蹤 好豪的粉絲專頁,我會持續撰寫 Python 技巧、資料科學等知識,與你分享!
也歡迎點選下方按鈕將本文加入書籤隨時複習、或者分享給更多正在學 Python 的朋友。