字典物件在Python中作為最常用的資料結構之一,和數字、字串、列表、元組並列為5大基本資料結構,字典中的元素通過鍵來存取,而非像列表一樣通過偏移存取。筆者總結了字典的一些常用Pyhonic用法,這是字典的Pythonic用法的上篇
0. 使用 in/not in 檢查 key 是否存在於字典
判斷某個 key 是否存在於字典中時,一般初學者想到的方法是,先以列表的形式把字典所有鍵返回,再判斷該key是否存在於鍵列表中:
1 2 3 4 5 6 |
dictionary = {} keys = dictionary.keys() for k in keys: if key == k: print True break |
更具Pythonic的用法是使用in
關鍵字來判斷 key 是否 存在於字典中:
1 2 3 4 |
if key in dictionary: print True else: print False |
1. 使用 setdefault() 初始化字典鍵值
使用字典的時候經常會遇到這樣一種應用場景:動態更新字典,像如下程式碼,如果 key 不在 dictionary 中那麼就新增它並把它對應的值初始為空列表 [] ,然後把元素 append 到空列表中:
1 2 3 4 |
dictionary = {} if "key" not in dictionary: dictionary["key"] = [] dictionary["key"].append("list_item") |
儘管這段程式碼沒有任何邏輯錯誤,但是我們可以使用setdefault
來實現更Pyhonic的寫法:
1 2 |
dictionary = {} dictionary.setdefault("key", []).append("list_item") |
字典呼叫 setdefault 方法時,首先檢查 key 是否存在,如果存在該方法什麼也不做,如果不存在 setdefault 就會建立該 key,並把第二個引數[]
作為 key 對應的值。
2. 使用 defaultdict() 初始化字典
初始化一個字典時,如果初始情況下希望所有 key 對應的值都是某個預設的初始值,比如有一批使用者的信用積分都初始為100,現在想給 a
使用者增加10分
1 2 3 4 |
d = {} if 'a' not in d: d['a'] = 100 d['a'] += 10 |
同樣這段程式碼也沒任何問題,換成更pyhtonic的寫法是:
1 2 3 |
from collections import defaultdict d = defaultdict(lambda: 100) d['a'] += 10 |
defaultdict 是位於 collections 模組下,它是 dict
類的子類,語法結構是:
1 |
class collections.defaultdict([default_factory[, ...]]) |
第一個引數default_factory
是一個工廠方法,每次初始化某個鍵的時候將會被呼叫,value就是default_factory
返回的值,剩下的引數和dict()
函式接收的引數一樣
3. 使用 iteritems() 迭代大資料
迭代大資料字典時,如果是使用 items() 方法,那麼在迭代之前,迭代器迭代前需要把資料完整地載入到記憶體,這種方式不僅處理非常慢而且浪費記憶體,下面程式碼約佔1.6G記憶體(為什麼是1.6G?可以參考:http://stackoverflow.com/questions/4279358/pythons-underlying-hash-data-structure-for-dictionaries)
1 2 3 |
d = {i: i * 2 for i in xrange(10000000)} for key, value in d.items(): print("{0} = {1}".format(key, value)) |
而使用 iteritem() 方法替換 items() ,最終實現的效果一樣,但是消耗的記憶體降低50%,為什麼差距那麼大呢?因為 items() 返回的是一個 list,list 在迭代的時候會預先把所有的元素載入到記憶體,而 iteritem() 返回的一個迭代器(iterators),迭代器在迭代的時候,迭代元素逐個的生成。
1 2 3 |
d = {i: i * 2 for i in xrange(10000000)} for key, value in d.iteritem(): print("{0} = {1}".format(key, value)) |
4. 高效合併字典
普通方法
合併多個字典的時候可以用一行程式碼實現:
1 2 3 |
x = {'a': 1, 'b': 2} y = {'b': 3, 'c': 4} z = dict(x.items() + y.items()) |
這種寫法看起來很Pythonic,但仔細分析的話,它的執行效率並不高,items()
方法在python2.7中返回的是列表物件,兩個列表相加得到一個新的列表,這樣記憶體中存在3個列表物件,如果兩個列表的大小都是1G,那麼執行這段程式碼需要佔用4G的空間來建立這個字典。此外這段程式碼在Python3中會報錯,因為python3中items()
返回的是dict_items
物件,而不是列表。
1 2 3 4 |
>>> c = dict(a.items() + b.items()) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for +: 'dict_items' and 'dict_items' |
在python3中,你需要明確地強制轉換成list物件:
1 |
z = dict(list(x.items()) + list(y.items())) |
Pythonic方法
在Python3.5中提供了一種全新的Pythonic方法:
1 |
z = {**x, **y} |
不過考慮到大部分系統還是基於Python2,所以一種更相容的pythonic方法是:
1 2 |
z = x.copy() z.update(y) |
當然,你可以把它封裝成一個函式:
1 2 3 4 5 6 7 8 9 10 |
def merge_dicts(*dict_args): ''' 可以接收1個或多個字典引數 ''' result = {} for dictionary in dict_args: result.update(dictionary) return result z = merge_dicts(a, b, c, d, e, f, g) |
其他方法
還有其他方式來合併字典,但是效能不一定是最優的,比如: python2.7可以支援字典推導式
1 |
{k: v for d in dicts for k, v in d.items()} |
python2.6及以下版本使用
1 |
dict((k, v) for d in dicts for k, v in d.items()) |
效能對比
1 2 3 4 5 6 7 8 9 |
import timeit >>> min(timeit.repeat(lambda: {**x, **y})) # python3.5 0.4094954460160807 >>> min(timeit.repeat(lambda: merge_two_dicts(x, y))) 0.5726828575134277 >>> min(timeit.repeat(lambda: {k: v for d in (x, y) for k, v in d.items()} )) 1.163769006729126 >>> min(timeit.repeat(lambda: dict((k, v) for d in (x, y) for k, v in d.items()))) 2.2345519065856934 |
直接使用python3.5中的{**x, **y}
是最快的,使用update
次之,用字典推導式相對來說是最慢的。