Python 提供了大量的內置數據結構,包括列表,集合以及字典。大多數狀況下使用這些數據結構是很簡單的。 可是,咱們也會常常碰到到諸如查詢,排序和過濾等等這些廣泛存在的問題。 所以,這一章的目的就是討論這些比較常見的問題和算法。 另外,咱們也會給出在集合模塊 collections
當中操做這些數據結構的方法。python
如今有一個包含 N 個元素的元組或者是序列,怎樣將它裏面的值解壓後同時賦值給 N 個變量?程序員
任何的序列(或者是可迭代對象)能夠經過一個簡單的賦值語句解壓並賦值給多個變量。 惟一的前提就是變量的數量必須跟序列元素的數量是同樣的。算法
代碼示例:數據庫
>>> p = (4, 5) >>> x, y = p >>> x 4 >>> y 5 >>> >>> data = [ 'ACME', 50, 91.1, (2012, 12, 21) ] >>> name, shares, price, date = data >>> name 'ACME' >>> date (2012, 12, 21) >>> name, shares, price, (year, mon, day) = data >>> name 'ACME' >>> year 2012 >>> mon 12 >>> day 21 >>>
若是變量個數和序列元素的個數不匹配,會產生一個異常。編程
代碼示例:json
>>> p = (4, 5)
>>> x, y, z = p
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: need more than 2 values to unpack
>>>
實際上,這種解壓賦值能夠用在任何可迭代對象上面,而不只僅是列表或者元組。 包括字符串,文件對象,迭代器和生成器。api
代碼示例:數據結構
>>> s = 'Hello' >>> a, b, c, d, e = s >>> a 'H' >>> b 'e' >>> e 'o' >>>
有時候,你可能只想解壓一部分,丟棄其餘的值。對於這種狀況 Python 並無提供特殊的語法。 可是你可使用任意變量名去佔位,到時候丟掉這些變量就好了。app
代碼示例:數據結構和算法
>>> data = [ 'ACME', 50, 91.1, (2012, 12, 21) ] >>> _, shares, price, _ = data >>> shares 50 >>> price 91.1 >>>
你必須保證你選用的那些佔位變量名在其餘地方沒被使用到。
若是一個可迭代對象的元素個數超過變量個數時,會拋出一個 ValueError
。 那麼怎樣才能從這個可迭代對象中解壓出 N 個元素出來?
Python 的星號表達式能夠用來解決這個問題。好比,你在學習一門課程,在學期末的時候, 你想統計下家庭做業的平均成績,可是排除掉第一個和最後一個分數。若是隻有四個分數,你可能就直接去簡單的手動賦值, 但若是有 24 個呢?這時候星號表達式就派上用場了:
def drop_first_last(grades): first, *middle, last = grades return avg(middle)
另一種狀況,假設你如今有一些用戶的記錄列表,每條記錄包含一個名字、郵件,接着就是不肯定數量的電話號碼。 你能夠像下面這樣分解這些記錄:
>>> record = ('Dave', 'dave@example.com', '773-555-1212', '847-555-1212') >>> name, email, *phone_numbers = record >>> name 'Dave' >>> email 'dave@example.com' >>> phone_numbers ['773-555-1212', '847-555-1212'] >>>
值得注意的是上面解壓出的 phone_numbers
變量永遠都是列表類型,無論解壓的電話號碼數量是多少(包括 0 個)。 因此,任何使用到 phone_numbers
變量的代碼就不須要作多餘的類型檢查去確認它是不是列表類型了。
星號表達式也能用在列表的開始部分。好比,你有一個公司前 8 個月銷售數據的序列, 可是你想看下最近一個月數據和前面 7 個月的平均值的對比。你能夠這樣作:
*trailing_qtrs, current_qtr = sales_record trailing_avg = sum(trailing_qtrs) / len(trailing_qtrs) return avg_comparison(trailing_avg, current_qtr)
下面是在 Python 解釋器中執行的結果:
>>> *trailing, current = [10, 8, 7, 1, 9, 5, 10, 3] >>> trailing [10, 8, 7, 1, 9, 5, 10] >>> current 3
擴展的迭代解壓語法是專門爲解壓不肯定個數或任意個數元素的可迭代對象而設計的。 一般,這些可迭代對象的元素結構有肯定的規則(好比第 1 個元素後面都是電話號碼), 星號表達式讓開發人員能夠很容易的利用這些規則來解壓出元素來。 而不是經過一些比較複雜的手段去獲取這些關聯的元素值。
值得注意的是,星號表達式在迭代元素爲可變長元組的序列時是頗有用的。 好比,下面是一個帶有標籤的元組序列:
records = [ ('foo', 1, 2), ('bar', 'hello'), ('foo', 3, 4), ] def do_foo(x, y): print('foo', x, y) def do_bar(s): print('bar', s) for tag, *args in records: if tag == 'foo': do_foo(*args) elif tag == 'bar': do_bar(*args)
星號解壓語法在字符串操做的時候也會頗有用,好比字符串的分割。
代碼示例:
>>> line = 'nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false' >>> uname, *fields, homedir, sh = line.split(':') >>> uname 'nobody' >>> homedir '/var/empty' >>> sh '/usr/bin/false' >>>
有時候,你想解壓一些元素後丟棄它們,你不能簡單就使用 *
, 可是你可使用一個普通的廢棄名稱,好比 _
或者 ign
(ignore)。
代碼示例:
>>> record = ('ACME', 50, 123.45, (12, 18, 2012)) >>> name, *_, (*_, year) = record >>> name 'ACME' >>> year 2012 >>>
在不少函數式語言中,星號解壓語法跟列表處理有許多類似之處。好比,若是你有一個列表, 你能夠很容易的將它分割成先後兩部分:
>>> items = [1, 10, 7, 4, 5, 9] >>> head, *tail = items >>> head 1 >>> tail [10, 7, 4, 5, 9] >>>
若是你夠聰明的話,還能用這種分割語法去巧妙的實現遞歸算法。好比:
>>> def sum(items): ... head, *tail = items ... return head + sum(tail) if tail else head ... >>> sum(items) 36 >>>
而後,因爲語言層面的限制,遞歸併非 Python 擅長的。 所以,最後那個遞歸演示僅僅是個好奇的探索罷了,對這個不要太認真了。
在迭代操做或者其餘操做的時候,怎樣只保留最後有限幾個元素的歷史記錄?
保留有限歷史記錄正是 collections.deque
大顯身手的時候。好比,下面的代碼在多行上面作簡單的文本匹配, 並返回匹配所在行的最後N行:
from collections import deque def search(lines, pattern, history=5): previous_lines = deque(maxlen=history) for line in lines: if pattern in line: yield line, previous_lines previous_lines.append(line) # Example use on a file if __name__ == '__main__': with open(r'../../cookbook/somefile.txt') as f: for line, prevlines in search(f, 'python', 5): for pline in prevlines: print(pline, end='') print(line, end='') print('-' * 20)
咱們在寫查詢元素的代碼時,一般會使用包含 yield
表達式的生成器函數,也就是咱們上面示例代碼中的那樣。 這樣能夠將搜索過程代碼和使用搜索結果代碼解耦。若是你還不清楚什麼是生成器。
使用 deque(maxlen=N)
構造函數會新建一個固定大小的隊列。當新的元素加入而且這個隊列已滿的時候, 最老的元素會自動被移除掉。
代碼示例:
>>> q = deque(maxlen=3) >>> q.append(1) >>> q.append(2) >>> q.append(3) >>> q deque([1, 2, 3], maxlen=3) >>> q.append(4) >>> q deque([2, 3, 4], maxlen=3) >>> q.append(5) >>> q deque([3, 4, 5], maxlen=3)
儘管你也能夠手動在一個列表上實現這一的操做(好比增長、刪除等等)。可是這裏的隊列方案會更加優雅而且運行得更快些。
更通常的, deque
類能夠被用在任何你只須要一個簡單隊列數據結構的場合。 若是你不設置最大隊列大小,那麼就會獲得一個無限大小隊列,你能夠在隊列的兩端執行添加和彈出元素的操做。
代碼示例:
>>> q = deque() >>> q.append(1) >>> q.append(2) >>> q.append(3) >>> q deque([1, 2, 3]) >>> q.appendleft(4) >>> q deque([4, 1, 2, 3]) >>> q.pop() 3 >>> q deque([4, 1, 2]) >>> q.popleft() 4
在隊列兩端插入或刪除元素時間複雜度都是 O(1)
,區別於列表,在列表的開頭插入或刪除元素的時間複雜度爲 O(N)
。
怎樣從一個集合中得到最大或者最小的 N 個元素列表?
heapq 模塊有兩個函數:nlargest()
和 nsmallest()
能夠完美解決這個問題。
import heapq nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2] print(heapq.nlargest(3, nums)) # Prints [42, 37, 23] print(heapq.nsmallest(3, nums)) # Prints [-4, 1, 2]
兩個函數都能接受一個關鍵字參數,用於更復雜的數據結構中:
portfolio = [ {'name': 'IBM', 'shares': 100, 'price': 91.1}, {'name': 'AAPL', 'shares': 50, 'price': 543.22}, {'name': 'FB', 'shares': 200, 'price': 21.09}, {'name': 'HPQ', 'shares': 35, 'price': 31.75}, {'name': 'YHOO', 'shares': 45, 'price': 16.35}, {'name': 'ACME', 'shares': 75, 'price': 115.65} ] cheap = heapq.nsmallest(3, portfolio, key=lambda s: s['price']) expensive = heapq.nlargest(3, portfolio, key=lambda s: s['price'])
譯者注:上面代碼在對每一個元素進行對比的時候,會以 price
的值進行比較。
若是你想在一個集合中查找最小或最大的 N 個元素,而且 N 小於集合元素數量,那麼這些函數提供了很好的性能。 由於在底層實現裏面,首先會先將集合數據進行堆排序後放入一個列表中:
>>> nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2] >>> import heapq >>> heap = list(nums) >>> heapq.heapify(heap) >>> heap [-4, 2, 1, 23, 7, 2, 18, 23, 42, 37, 8] >>>
堆數據結構最重要的特徵是 heap[0]
永遠是最小的元素。而且剩餘的元素能夠很容易的經過調用 heapq.heappop()
方法獲得, 該方法會先將第一個元素彈出來,而後用下一個最小的元素來取代被彈出元素(這種操做時間複雜度僅僅是 O(log N),N 是堆大小)。 好比,若是想要查找最小的 3 個元素,你能夠這樣作:
>>> heapq.heappop(heap) -4 >>> heapq.heappop(heap) 1 >>> heapq.heappop(heap) 2
當要查找的元素個數相對比較小的時候,函數 nlargest()
和 nsmallest()
是很合適的。 若是你僅僅想查找惟一的最小或最大(N=1)的元素的話,那麼使用 min()
和 max()
函數會更快些。 相似的,若是 N 的大小和集合大小接近的時候,一般先排序這個集合而後再使用切片操做會更快點 ( sorted(items)[:N]
或者是 sorted(items)[-N:]
)。 須要在正確場合使用函數 nlargest()
和 nsmallest()
才能發揮它們的優點 (若是 N 快接近集合大小了,那麼使用排序操做會更好些)。
儘管你沒有必要必定使用這裏的方法,可是堆數據結構的實現是一個頗有趣而且值得你深刻學習的東西。 基本上只要是數據結構和算法書籍裏面都會有說起到。 heapq
模塊的官方文檔裏面也詳細的介紹了堆數據結構底層的實現細節。
怎樣實現一個按優先級排序的隊列? 而且在這個隊列上面每次 pop 操做老是返回優先級最高的那個元素
下面的類利用 heapq
模塊實現了一個簡單的優先級隊列:
import heapq class PriorityQueue: def __init__(self): self._queue = [] self._index = 0 def push(self, item, priority): heapq.heappush(self._queue, (-priority, self._index, item)) self._index += 1 def pop(self): return heapq.heappop(self._queue)[-1]
下面是它的使用方式:
>>> class Item: ... def __init__(self, name): ... self.name = name ... def __repr__(self): ... return 'Item({!r})'.format(self.name) ... >>> q = PriorityQueue() >>> q.push(Item('foo'), 1) >>> q.push(Item('bar'), 5) >>> q.push(Item('spam'), 4) >>> q.push(Item('grok'), 1) >>> q.pop() Item('bar') >>> q.pop() Item('spam') >>> q.pop() Item('foo') >>> q.pop() Item('grok') >>>
仔細觀察能夠發現,第一個 pop()
操做返回優先級最高的元素。 另外注意到若是兩個有着相同優先級的元素( foo
和 grok
),pop 操做按照它們被插入到隊列的順序返回的。
這一小節咱們主要關注 heapq
模塊的使用。 函數 heapq.heappush()
和 heapq.heappop()
分別在隊列 _queue
上插入和刪除第一個元素, 而且隊列 _queue
保證第一個元素擁有最高優先級。 heappop()
函數老是返回」最小的」的元素,這就是保證隊列pop操做返回正確元素的關鍵。 另外,因爲 push 和 pop 操做時間複雜度爲 O(log N),其中 N 是堆的大小,所以就算是 N 很大的時候它們運行速度也依舊很快。
在上面代碼中,隊列包含了一個 (-priority, index, item)
的元組。 優先級爲負數的目的是使得元素按照優先級從高到低排序。 這個跟普通的按優先級從低到高排序的堆排序恰巧相反。
index
變量的做用是保證同等優先級元素的正確排序。 經過保存一個不斷增長的 index
下標變量,能夠確保元素按照它們插入的順序排序。 並且, index
變量也在相同優先級元素比較的時候起到重要做用。
爲了闡明這些,先假定 Item
實例是不支持排序的:
>>> a = Item('foo')
>>> b = Item('bar')
>>> a < b
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unorderable types: Item() < Item()
>>>
若是你使用元組 (priority, item)
,只要兩個元素的優先級不一樣就能比較。 可是若是兩個元素優先級同樣的話,那麼比較操做就會跟以前同樣出錯:
>>> a = (1, Item('foo'))
>>> b = (5, Item('bar'))
>>> a < b
True
>>> c = (1, Item('grok'))
>>> a < c
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unorderable types: Item() < Item()
>>>
經過引入另外的 index
變量組成三元組 (priority, index, item)
,就能很好的避免上面的錯誤, 由於不可能有兩個元素有相同的 index
值。Python 在作元組比較時候,若是前面的比較已經能夠肯定結果了, 後面的比較操做就不會發生了:
>>> a = (1, 0, Item('foo')) >>> b = (5, 1, Item('bar')) >>> c = (1, 2, Item('grok')) >>> a < b True >>> a < c True >>>
若是你想在多個線程中使用同一個隊列,那麼你須要增長適當的鎖和信號量機制。
heapq
模塊的官方文檔有更詳細的例子程序以及對於堆理論及其實現的詳細說明。
怎樣實現一個鍵對應多個值的字典(也叫 multidict
)?
一個字典就是一個鍵對應一個單值的映射。若是你想要一個鍵映射多個值,那麼你就須要將這多個值放到另外的容器中, 好比列表或者集合裏面。好比,你能夠像下面這樣構造這樣的字典:
d = { 'a' : [1, 2, 3], 'b' : [4, 5] } e = { 'a' : {1, 2, 3}, 'b' : {4, 5} }
選擇使用列表仍是集合取決於你的實際需求。若是你想保持元素的插入順序就應該使用列表, 若是想去掉重複元素就使用集合(而且不關心元素的順序問題)。
你能夠很方便的使用 collections
模塊中的 defaultdict
來構造這樣的字典。 defaultdict
的一個特徵是它會自動初始化每一個 key
剛開始對應的值,因此你只須要關注添加元素操做了。好比:
from collections import defaultdict d = defaultdict(list) d['a'].append(1) d['a'].append(2) d['b'].append(4) d = defaultdict(set) d['a'].add(1) d['a'].add(2) d['b'].add(4)
須要注意的是, defaultdict
會自動爲將要訪問的鍵(就算目前字典中並不存在這樣的鍵)建立映射實體。 若是你並不須要這樣的特性,你能夠在一個普通的字典上使用 setdefault()
方法來代替。好比:
d = {} # 一個普通的字典 d.setdefault('a', []).append(1) d.setdefault('a', []).append(2) d.setdefault('b', []).append(4)
可是不少程序員以爲 setdefault()
用起來有點彆扭。由於每次調用都得建立一個新的初始值的實例(例子程序中的空列表 []
)。
通常來說,建立一個多值映射字典是很簡單的。可是,若是你選擇本身實現的話,那麼對於值的初始化可能會有點麻煩, 你可能會像下面這樣來實現:
d = {} for key, value in pairs: if key not in d: d[key] = [] d[key].append(value)
若是使用 defaultdict
的話代碼就更加簡潔了:
d = defaultdict(list) for key, value in pairs: d[key].append(value)
這一小節所討論的問題跟數據處理中的記錄歸類問題有大的關聯。
你想建立一個字典,而且在迭代或序列化這個字典的時候可以控制元素的順序。
爲了能控制一個字典中元素的順序,你可使用 collections
模塊中的 OrderedDict
類。 在迭代操做的時候它會保持元素被插入時的順序,示例以下:
from collections import OrderedDict d = OrderedDict() d['foo'] = 1 d['bar'] = 2 d['spam'] = 3 d['grok'] = 4 # Outputs "foo 1", "bar 2", "spam 3", "grok 4" for key in d: print(key, d[key])
當你想要構建一個未來須要序列化或編碼成其餘格式的映射的時候, OrderedDict
是很是有用的。 好比,你想精確控制以 JSON 編碼後字段的順序,你能夠先使用 OrderedDict
來構建這樣的數據:
>>> import json >>> json.dumps(d) '{"foo": 1, "bar": 2, "spam": 3, "grok": 4}' >>>
OrderedDict
內部維護着一個根據鍵插入順序排序的雙向鏈表。每次當一個新的元素插入進來的時候, 它會被放到鏈表的尾部。對於一個已經存在的鍵的重複賦值不會改變鍵的順序。
須要注意的是,一個 OrderedDict
的大小是一個普通字典的兩倍,由於它內部維護着另一個鏈表。 因此若是你要構建一個須要大量 OrderedDict
實例的數據結構的時候(好比讀取 100,000 行 CSV 數據到一個 OrderedDict
列表中去), 那麼你就得仔細權衡一下是否使用 OrderedDict
帶來的好處要大過額外內存消耗的影響。
怎樣在數據字典中執行一些計算操做(好比求最小值、最大值、排序等等)?
考慮下面的股票名和價格映射字典:
prices = { 'ACME': 45.23, 'AAPL': 612.78, 'IBM': 205.55, 'HPQ': 37.20, 'FB': 10.75 }
爲了對字典值執行計算操做,一般須要使用 zip()
函數先將鍵和值反轉過來。 好比,下面是查找最小和最大股票價格和股票值的代碼:
min_price = min(zip(prices.values(), prices.keys())) # min_price is (10.75, 'FB') max_price = max(zip(prices.values(), prices.keys())) # max_price is (612.78, 'AAPL')
相似的,可使用 zip()
和 sorted()
函數來排列字典數據:
prices_sorted = sorted(zip(prices.values(), prices.keys())) # prices_sorted is [(10.75, 'FB'), (37.2, 'HPQ'), # (45.23, 'ACME'), (205.55, 'IBM'), # (612.78, 'AAPL')]
執行這些計算的時候,須要注意的是 zip()
函數建立的是一個只能訪問一次的迭代器。 好比,下面的代碼就會產生錯誤:
prices_and_names = zip(prices.values(), prices.keys()) print(min(prices_and_names)) # OK print(max(prices_and_names)) # ValueError: max() arg is an empty sequence
若是你在一個字典上執行普通的數學運算,你會發現它們僅僅做用於鍵,而不是值。好比:
min(prices) # Returns 'AAPL' max(prices) # Returns 'IBM'
這個結果並非你想要的,由於你想要在字典的值集合上執行這些計算。 或許你會嘗試着使用字典的 values()
方法來解決這個問題:
min(prices.values()) # Returns 10.75 max(prices.values()) # Returns 612.78
不幸的是,一般這個結果一樣也不是你想要的。 你可能還想要知道對應的鍵的信息(好比那種股票價格是最低的?)。
你能夠在 min()
和 max()
函數中提供 key
函數參數來獲取最小值或最大值對應的鍵的信息。好比:
min(prices, key=lambda k: prices[k]) # Returns 'FB' max(prices, key=lambda k: prices[k]) # Returns 'AAPL'
可是,若是還想要獲得最小值,你又得執行一次查找操做。好比:
min_value = prices[min(prices, key=lambda k: prices[k])]
前面的 zip()
函數方案經過將字典」反轉」爲 (值,鍵) 元組序列來解決了上述問題。 當比較兩個元組的時候,值會先進行比較,而後纔是鍵。 這樣的話你就能經過一條簡單的語句就能很輕鬆的實如今字典上的求最值和排序操做了。
須要注意的是在計算操做中使用到了 (值,鍵) 對。當多個實體擁有相同的值的時候,鍵會決定返回結果。 好比,在執行 min()
和 max()
操做的時候,若是恰巧最小或最大值有重複的,那麼擁有最小或最大鍵的實體會返回:
>>> prices = { 'AAA' : 45.23, 'ZZZ': 45.23 } >>> min(zip(prices.values(), prices.keys())) (45.23, 'AAA') >>> max(zip(prices.values(), prices.keys())) (45.23, 'ZZZ') >>>
怎樣在兩個字典中尋尋找相同點(好比相同的鍵、相同的值等等)?
考慮下面兩個字典:
a = { 'x' : 1, 'y' : 2, 'z' : 3 } b = { 'w' : 10, 'x' : 11, 'y' : 2 }
爲了尋找兩個字典的相同點,能夠簡單的在兩字典的 keys()
或者 items()
方法返回結果上執行集合操做。好比:
# Find keys in common
a.keys() & b.keys() # { 'x', 'y' } # Find keys in a that are not in b a.keys() - b.keys() # { 'z' } # Find (key,value) pairs in common a.items() & b.items() # { ('y', 2) }
這些操做也能夠用於修改或者過濾字典元素。 好比,假如你想以現有字典構造一個排除幾個指定鍵的新字典。 下面利用字典推導來實現這樣的需求:
# Make a new dictionary with certain keys removed
c = {key:a[key] for key in a.keys() - {'z', 'w'}} # c is {'x': 1, 'y': 2}
一個字典就是一個鍵集合與值集合的映射關係。 字典的 keys()
方法返回一個展示鍵集合的鍵視圖對象。 鍵視圖的一個不多被瞭解的特性就是它們也支持集合操做,好比集合並、交、差運算。 因此,若是你想對集合的鍵執行一些普通的集合操做,能夠直接使用鍵視圖對象而不用先將它們轉換成一個 set。
字典的 items()
方法返回一個包含 (鍵,值) 對的元素視圖對象。 這個對象一樣也支持集合操做,而且能夠被用來查找兩個字典有哪些相同的鍵值對。
儘管字典的 values()
方法也是相似,可是它並不支持這裏介紹的集合操做。 某種程度上是由於值視圖不能保證全部的值互不相同,這樣會致使某些集合操做會出現問題。 不過,若是你硬要在值上面執行這些集合操做的話,你能夠先將值集合轉換成 set,而後再執行集合運算就好了。
怎樣在一個序列上面保持元素順序的同時消除重複的值?
若是序列上的值都是 hashable
類型,那麼能夠很簡單的利用集合或者生成器來解決這個問題。好比:
def dedupe(items): seen = set() for item in items: if item not in seen: yield item seen.add(item)
下面是使用上述函數的例子:
>>> a = [1, 5, 2, 1, 9, 1, 5, 10] >>> list(dedupe(a)) [1, 5, 2, 9, 10] >>>
這個方法僅僅在序列中元素爲 hashable
的時候才管用。 若是你想消除元素不可哈希(好比 dict
類型)的序列中重複元素的話,你須要將上述代碼稍微改變一下,就像這樣:
def dedupe(items, key=None): seen = set() for item in items: val = item if key is None else key(item) if val not in seen: yield item seen.add(val)
這裏的key參數指定了一個函數,將序列元素轉換成 hashable
類型。下面是它的用法示例:
>>> a = [ {'x':1, 'y':2}, {'x':1, 'y':3}, {'x':1, 'y':2}, {'x':2, 'y':4}] >>> list(dedupe(a, key=lambda d: (d['x'],d['y']))) [{'x': 1, 'y': 2}, {'x': 1, 'y': 3}, {'x': 2, 'y': 4}] >>> list(dedupe(a, key=lambda d: d['x'])) [{'x': 1, 'y': 2}, {'x': 2, 'y': 4}] >>>
若是你想基於單個字段、屬性或者某個更大的數據結構來消除重複元素,第二種方案一樣能夠勝任。
若是你僅僅就是想消除重複元素,一般能夠簡單的構造一個集合。好比:
>>> a [1, 5, 2, 1, 9, 1, 5, 10] >>> set(a) {1, 2, 10, 5, 9} >>>
然而,這種方法不能維護元素的順序,生成的結果中的元素位置被打亂。而上面的方法能夠避免這種狀況。
在本節中咱們使用了生成器函數讓咱們的函數更加通用,不只僅是侷限於列表處理。 好比,若是若是你想讀取一個文件,消除重複行,你能夠很容易像這樣作:
with open(somefile,'r') as f: for line in dedupe(f): ...
上述key函數參數模仿了 sorted()
, min()
和 max()
等內置函數的類似功能。
若是你的程序包含了大量沒法直視的硬編碼切片,而且你想清理一下代碼。
假定你要從一個記錄(好比文件或其餘相似格式)中的某些固定位置提取字段:
###### 0123456789012345678901234567890123456789012345678901234567890'
record = '....................100 .......513.25 ..........' cost = int(record[20:23]) * float(record[31:37])
與其那樣寫,爲何不想這樣命名切片呢:
SHARES = slice(20, 23) PRICE = slice(31, 37) cost = int(record[SHARES]) * float(record[PRICE])
在這個版本中,你避免了使用大量難以理解的硬編碼下標。這使得你的代碼更加清晰可讀。
通常來說,代碼中若是出現大量的硬編碼下標會使得代碼的可讀性和可維護性大大下降。 好比,若是你回過來看看一年前你寫的代碼,你會摸着腦殼想那時候本身到底想幹嗎啊。 這是一個很簡單的解決方案,它讓你更加清晰的表達代碼的目的。
內置的 slice()
函數建立了一個切片對象。全部使用切片的地方均可以使用切片對象。好比:
>>> items = [0, 1, 2, 3, 4, 5, 6] >>> a = slice(2, 4) >>> items[2:4] [2, 3] >>> items[a] [2, 3] >>> items[a] = [10,11] >>> items [0, 1, 10, 11, 4, 5, 6] >>> del items[a] >>> items [0, 1, 4, 5, 6]
若是你有一個切片對象a,你能夠分別調用它的 a.start
, a.stop
, a.step
屬性來獲取更多的信息。好比:
>>> a = slice(5, 50, 2) >>> a.start 5 >>> a.stop 50 >>> a.step 2 >>>
另外,你還能夠經過調用切片的 indices(size)
方法將它映射到一個已知大小的序列上。 這個方法返回一個三元組 (start, stop, step)
,全部的值都會被縮小,直到適合這個已知序列的邊界爲止。 這樣,使用的時就不會出現 IndexError
異常。好比:
>>> s = 'HelloWorld' >>> a.indices(len(s)) (5, 10, 2) >>> for i in range(*a.indices(len(s))): ... print(s[i]) ... W r d >>>
怎樣找出一個序列中出現次數最多的元素呢?
collections.Counter
類就是專門爲這類問題而設計的, 它甚至有一個有用的 most_common()
方法直接給了你答案。
爲了演示,先假設你有一個單詞列表而且想找出哪一個單詞出現頻率最高。你能夠這樣作:
words = [ 'look', 'into', 'my', 'eyes', 'look', 'into', 'my', 'eyes', 'the', 'eyes', 'the', 'eyes', 'the', 'eyes', 'not', 'around', 'the', 'eyes', "don't", 'look', 'around', 'the', 'eyes', 'look', 'into', 'my', 'eyes', "you're", 'under' ] from collections import Counter word_counts = Counter(words) # 出現頻率最高的3個單詞 top_three = word_counts.most_common(3) print(top_three) # Outputs [('eyes', 8), ('the', 5), ('look', 4)]
做爲輸入, Counter
對象能夠接受任意的由可哈希(hashable
)元素構成的序列對象。 在底層實現上,一個 Counter
對象就是一個字典,將元素映射到它出現的次數上。好比:
>>> word_counts['not'] 1 >>> word_counts['eyes'] 8 >>>
若是你想手動增長計數,能夠簡單的用加法:
>>> morewords = ['why','are','you','not','looking','in','my','eyes'] >>> for word in morewords: ... word_counts[word] += 1 ... >>> word_counts['eyes'] 9 >>>
或者你可使用 update()
方法:
>>> word_counts.update(morewords) >>>
Counter
實例一個不爲人知的特性是它們能夠很容易的跟數學運算操做相結合。好比:
>>> a = Counter(words) >>> b = Counter(morewords) >>> a Counter({'eyes': 8, 'the': 5, 'look': 4, 'into': 3, 'my': 3, 'around': 2, "you're": 1, "don't": 1, 'under': 1, 'not': 1}) >>> b Counter({'eyes': 1, 'looking': 1, 'are': 1, 'in': 1, 'not': 1, 'you': 1, 'my': 1, 'why': 1}) >>> # Combine counts >>> c = a + b >>> c Counter({'eyes': 9, 'the': 5, 'look': 4, 'my': 4, 'into': 3, 'not': 2, 'around': 2, "you're": 1, "don't": 1, 'in': 1, 'why': 1, 'looking': 1, 'are': 1, 'under': 1, 'you': 1}) >>> # Subtract counts >>> d = a - b >>> d Counter({'eyes': 7, 'the': 5, 'look': 4, 'into': 3, 'my': 2, 'around': 2, "you're": 1, "don't": 1, 'under': 1}) >>>
毫無疑問, Counter
對象在幾乎全部須要製表或者計數數據的場合是很是有用的工具。 在解決這類問題的時候你應該優先選擇它,而不是手動的利用字典去實現。
你有一個字典列表,你想根據某個或某幾個字典字段來排序這個列表。
經過使用 operator
模塊的 itemgetter
函數,能夠很是容易的排序這樣的數據結構。 假設你從數據庫中檢索出來網站會員信息列表,而且如下列的數據結構返回:
rows = [ {'fname': 'Brian', 'lname': 'Jones', 'uid': 1003}, {'fname': 'David', 'lname': 'Beazley', 'uid': 1002}, {'fname': 'John', 'lname': 'Cleese', 'uid': 1001}, {'fname': 'Big', 'lname': 'Jones', 'uid': 1004} ]
根據任意的字典字段來排序輸入結果行是很容易實現的,代碼示例:
from operator import itemgetter rows_by_fname = sorted(rows, key=itemgetter('fname')) rows_by_uid = sorted(rows, key=itemgetter('uid')) print(rows_by_fname) print(rows_by_uid)
代碼的輸出以下:
[{'fname': 'Big', 'uid': 1004, 'lname': 'Jones'}, {'fname': 'Brian', 'uid': 1003, 'lname': 'Jones'}, {'fname': 'David', 'uid': 1002, 'lname': 'Beazley'}, {'fname': 'John', 'uid': 1001, 'lname': 'Cleese'}] [{'fname': 'John', 'uid': 1001, 'lname': 'Cleese'}, {'fname': 'David', 'uid': 1002, 'lname': 'Beazley'}, {'fname': 'Brian', 'uid': 1003, 'lname': 'Jones'}, {'fname': 'Big', 'uid': 1004, 'lname': 'Jones'}]
itemgetter()
函數也支持多個 keys,好比下面的代碼
rows_by_lfname = sorted(rows, key=itemgetter('lname','fname')) print(rows_by_lfname)
會產生以下的輸出:
[{'fname': 'David', 'uid': 1002, 'lname': 'Beazley'}, {'fname': 'John', 'uid': 1001, 'lname': 'Cleese'}, {'fname': 'Big', 'uid': 1004, 'lname': 'Jones'}, {'fname': 'Brian', 'uid': 1003, 'lname': 'Jones'}]
在上面例子中, rows
被傳遞給接受一個關鍵字參數的 sorted()
內置函數。 這個參數是 callable
類型,而且從 rows
中接受一個單一元素,而後返回被用來排序的值。 itemgetter()
函數就是負責建立這個 callable
對象的。
operator.itemgetter()
函數有一個被 rows
中的記錄用來查找值的索引參數。能夠是一個字典鍵名稱, 一個整形值或者任何可以傳入一個對象的 __getitem__()
方法的值。 若是你傳入多個索引參數給 itemgetter()
,它生成的 callable
對象會返回一個包含全部元素值的元組, 而且 sorted()
函數會根據這個元組中元素順序去排序。 但你想要同時在幾個字段上面進行排序(好比經過姓和名來排序,也就是例子中的那樣)的時候這種方法是頗有用的。
itemgetter()
有時候也能夠用 lambda
表達式代替,好比:
rows_by_fname = sorted(rows, key=lambda r: r['fname']) rows_by_lfname = sorted(rows, key=lambda r: (r['lname'],r['fname']))
這種方案也不錯。可是,使用 itemgetter()
方式會運行的稍微快點。所以,若是你對性能要求比較高的話就使用 itemgetter()
方式。
最後,不要忘了這節中展現的技術也一樣適用於 min()
和 max()
等函數。好比:
>>> min(rows, key=itemgetter('uid')) {'fname': 'John', 'lname': 'Cleese', 'uid': 1001} >>> max(rows, key=itemgetter('uid')) {'fname': 'Big', 'lname': 'Jones', 'uid': 1004} >>>
你想排序類型相同的對象,可是他們不支持原生的比較操做。
內置的 sorted()
函數有一個關鍵字參數 key
,能夠傳入一個 callable
對象給它, 這個 callable
對象對每一個傳入的對象返回一個值,這個值會被 sorted
用來排序這些對象。 好比,若是你在應用程序裏面有一個 User
實例序列,而且你但願經過他們的 user_id
屬性進行排序, 你能夠提供一個以 User
實例做爲輸入並輸出對應 user_id
值的 callable
對象。好比:
class User: def __init__(self, user_id): self.user_id = user_id def __repr__(self): return 'User({})'.format(self.user_id) def sort_notcompare(): users = [User(23), User(3), User(99)] print(users) print(sorted(users, key=lambda u: u.user_id))
另一種方式是使用 operator.attrgetter()
來代替 lambda 函數:
>>> from operator import attrgetter >>> sorted(users, key=attrgetter('user_id')) [User(3), User(23), User(99)] >>>
選擇使用 lambda 函數或者是 attrgetter()
可能取決於我的喜愛。 可是, attrgetter()
函數一般會運行的快點,而且還能同時容許多個字段進行比較。 這個跟 operator.itemgetter()
函數做用於字典類型很相似。 例如,若是 User
實例還有一個 first_name
和 last_name
屬性,那麼能夠向下面這樣排序:
by_name = sorted(users, key=attrgetter('last_name', 'first_name'))
一樣須要注意的是,這一小節用到的技術一樣適用於像 min()
和 max()
之類的函數。好比:
>>> min(users, key=attrgetter('user_id')) User(3) >>> max(users, key=attrgetter('user_id')) User(99) >>>
你有一個字典或者實例的序列,而後你想根據某個特定的字段好比 date
來分組迭代訪問。
itertools.groupby()
函數對於這樣的數據分組操做很是實用。 爲了演示,假設你已經有了下列的字典列表:
rows = [ {'address': '5412 N CLARK', 'date': '07/01/2012'}, {'address': '5148 N CLARK', 'date': '07/04/2012'}, {'address': '5800 E 58TH', 'date': '07/02/2012'}, {'address': '2122 N CLARK', 'date': '07/03/2012'}, {'address': '5645 N RAVENSWOOD', 'date': '07/02/2012'}, {'address': '1060 W ADDISON', 'date': '07/02/2012'}, {'address': '4801 N BROADWAY', 'date': '07/01/2012'}, {'address': '1039 W GRANVILLE', 'date': '07/04/2012'}, ]
如今假設你想在按 date 分組後的數據塊上進行迭代。爲了這樣作,你首先須要按照指定的字段(這裏就是 date
)排序, 而後調用 itertools.groupby()
函數:
from operator import itemgetter from itertools import groupby # Sort by the desired field first rows.sort(key=itemgetter('date')) # Iterate in groups for date, items in groupby(rows, key=itemgetter('date')): print(date) for i in items: print(' ', i)
運行結果:
07/01/2012 {'date': '07/01/2012', 'address': '5412 N CLARK'} {'date': '07/01/2012', 'address': '4801 N BROADWAY'} 07/02/2012 {'date': '07/02/2012', 'address': '5800 E 58TH'} {'date': '07/02/2012', 'address': '5645 N RAVENSWOOD'} {'date': '07/02/2012', 'address': '1060 W ADDISON'} 07/03/2012 {'date': '07/03/2012', 'address': '2122 N CLARK'} 07/04/2012 {'date': '07/04/2012', 'address': '5148 N CLARK'} {'date': '07/04/2012', 'address': '1039 W GRANVILLE'}
groupby()
函數掃描整個序列而且查找連續相同值(或者根據指定 key 函數返回值相同)的元素序列。 在每次迭代的時候,它會返回一個值和一個迭代器對象, 這個迭代器對象能夠生成元素值所有等於上面那個值的組中全部對象。
一個很是重要的準備步驟是要根據指定的字段將數據排序。 由於 groupby()
僅僅檢查連續的元素,若是事先並無排序完成的話,分組函數將得不到想要的結果。
若是你僅僅只是想根據 date
字段將數據分組到一個大的數據結構中去,而且容許隨機訪問, 那麼你最好使用 defaultdict()
來構建一個多值字典,好比:
from collections import defaultdict rows_by_date = defaultdict(list) for row in rows: rows_by_date[row['date']].append(row)
這樣的話你能夠很輕鬆的就能對每一個指定日期訪問對應的記錄:
>>> for r in rows_by_date['07/01/2012']: ... print(r) ... {'date': '07/01/2012', 'address': '5412 N CLARK'} {'date': '07/01/2012', 'address': '4801 N BROADWAY'} >>>
在上面這個例子中,咱們沒有必要先將記錄排序。所以,若是對內存佔用不是很關心, 這種方式會比先排序而後再經過 groupby()
函數迭代的方式運行得快一些。
你有一個數據序列,想利用一些規則從中提取出須要的值或者是縮短序列
最簡單的過濾序列元素的方法就是使用列表推導。好比:
>>> mylist = [1, 4, -5, 10, -7, 2, 3, -1] >>> [n for n in mylist if n > 0] [1, 4, 10, 2, 3] >>> [n for n in mylist if n < 0] [-5, -7, -1] >>>
使用列表推導的一個潛在缺陷就是若是輸入很是大的時候會產生一個很是大的結果集,佔用大量內存。 若是你對內存比較敏感,那麼你可使用生成器表達式迭代產生過濾的元素。好比:
>>> pos = (n for n in mylist if n > 0) >>> pos <generator object <genexpr> at 0x1006a0eb0> >>> for x in pos: ... print(x) ... 1 4 10 2 3 >>>
有時候,過濾規則比較複雜,不能簡單的在列表推導或者生成器表達式中表達出來。 好比,假設過濾的時候須要處理一些異常或者其餘複雜狀況。這時候你能夠將過濾代碼放到一個函數中, 而後使用內建的 filter()
函數。示例以下:
values = ['1', '2', '-3', '-', '4', 'N/A', '5'] def is_int(val): try: x = int(val) return True except ValueError: return False ivals = list(filter(is_int, values)) print(ivals) # Outputs ['1', '2', '-3', '4', '5']
filter()
函數建立了一個迭代器,所以若是你想獲得一個列表的話,就得像示例那樣使用 list()
去轉換。
列表推導和生成器表達式一般狀況下是過濾數據最簡單的方式。 其實它們還能在過濾的時候轉換數據。好比:
>>> mylist = [1, 4, -5, 10, -7, 2, 3, -1] >>> import math >>> [math.sqrt(n) for n in mylist if n > 0] [1.0, 2.0, 3.1622776601683795, 1.4142135623730951, 1.7320508075688772] >>>
過濾操做的一個變種就是將不符合條件的值用新的值代替,而不是丟棄它們。 好比,在一列數據中你可能不只想找到正數,並且還想將不是正數的數替換成指定的數。 經過將過濾條件放到條件表達式中去,能夠很容易的解決這個問題,就像這樣:
>>> clip_neg = [n if n > 0 else 0 for n in mylist] >>> clip_neg [1, 4, 0, 10, 0, 2, 3, 0] >>> clip_pos = [n if n < 0 else 0 for n in mylist] >>> clip_pos [0, 0, -5, 0, -7, 0, 0, -1] >>>
另一個值得關注的過濾工具就是 itertools.compress()
, 它以一個 iterable
對象和一個相對應的 Boolean
選擇器序列做爲輸入參數。 而後輸出 iterable
對象中對應選擇器爲 True
的元素。 當你須要用另一個相關聯的序列來過濾某個序列的時候,這個函數是很是有用的。 好比,假如如今你有下面兩列數據:
addresses = [ '5412 N CLARK', '5148 N CLARK', '5800 E 58TH', '2122 N CLARK', '5645 N RAVENSWOOD', '1060 W ADDISON', '4801 N BROADWAY', '1039 W GRANVILLE', ] counts = [ 0, 3, 10, 4, 1, 7, 6, 1]
如今你想將那些對應 count
值大於5的地址所有輸出,那麼你能夠這樣作:
>>> from itertools import compress >>> more5 = [n > 5 for n in counts] >>> more5 [False, False, True, False, False, True, True, False] >>> list(compress(addresses, more5)) ['5800 E 58TH', '1060 W ADDISON', '4801 N BROADWAY'] >>>
這裏的關鍵點在於先建立一個 Boolean
序列,指示哪些元素符合條件。 而後 compress()
函數根據這個序列去選擇輸出對應位置爲 True
的元素。
和 filter()
函數相似, compress()
也是返回的一個迭代器。所以,若是你須要獲得一個列表, 那麼你須要使用 list()
來將結果轉換爲列表類型。
你想構造一個字典,它是另一個字典的子集。
最簡單的方式是使用字典推導。好比:
prices = { 'ACME': 45.23, 'AAPL': 612.78, 'IBM': 205.55, 'HPQ': 37.20, 'FB': 10.75 } # Make a dictionary of all prices over 200 p1 = {key: value for key, value in prices.items() if value > 200} # Make a dictionary of tech stocks tech_names = {'AAPL', 'IBM', 'HPQ', 'MSFT'} p2 = {key: value for key, value in prices.items() if key in tech_names}
大多數狀況下字典推導能作到的,經過建立一個元組序列而後把它傳給 dict()
函數也能實現。好比:
p1 = dict((key, value) for key, value in prices.items() if value > 200)
可是,字典推導方式表意更清晰,而且實際上也會運行的更快些 (在這個例子中,實際測試幾乎比 dict()
函數方式快整整一倍)。
有時候完成同一件事會有多種方式。好比,第二個例子程序也能夠像這樣重寫:
# Make a dictionary of tech stocks
tech_names = { 'AAPL', 'IBM', 'HPQ', 'MSFT' } p2 = { key:prices[key] for key in prices.keys() & tech_names }
可是,運行時間測試結果顯示這種方案大概比第一種方案慢 1.6 倍。 若是對程序運行性能要求比較高的話,須要花點時間去作計時測試。
你有一段經過下標訪問列表或者元組中元素的代碼,可是這樣有時候會使得你的代碼難以閱讀, 因而你想經過名稱來訪問元素。
collections.namedtuple()
函數經過使用一個普通的元組對象來幫你解決這個問題。 這個函數其實是一個返回 Python 中標準元組類型子類的一個工廠方法。 你須要傳遞一個類型名和你須要的字段給它,而後它就會返回一個類,你能夠初始化這個類,爲你定義的字段傳遞值等。 代碼示例:
>>> from collections import namedtuple >>> Subscriber = namedtuple('Subscriber', ['addr', 'joined']) >>> sub = Subscriber('jonesy@example.com', '2012-10-19') >>> sub Subscriber(addr='jonesy@example.com', joined='2012-10-19') >>> sub.addr 'jonesy@example.com' >>> sub.joined '2012-10-19' >>>
儘管 namedtuple
的實例看起來像一個普通的類實例,可是它跟元組類型是可交換的,支持全部的普通元組操做,好比索引和解壓。 好比:
>>> len(sub) 2 >>> addr, joined = sub >>> addr 'jonesy@example.com' >>> joined '2012-10-19' >>>
命名元組的一個主要用途是將你的代碼從下標操做中解脫出來。 所以,若是你從數據庫調用中返回了一個很大的元組列表,經過下標去操做其中的元素, 當你在表中添加了新的列的時候你的代碼可能就會出錯了。可是若是你使用了命名元組,那麼就不會有這樣的顧慮。
爲了說明清楚,下面是使用普通元組的代碼:
def compute_cost(records): total = 0.0 for rec in records: total += rec[1] * rec[2] return total
下標操做一般會讓代碼表意不清晰,而且很是依賴記錄的結構。 下面是使用命名元組的版本:
from collections import namedtuple Stock = namedtuple('Stock', ['name', 'shares', 'price']) def compute_cost(records): total = 0.0 for rec in records: s = Stock(*rec) total += s.shares * s.price return total
命名元組另外一個用途就是做爲字典的替代,由於字典存儲須要更多的內存空間。 若是你須要構建一個很是大的包含字典的數據結構,那麼使用命名元組會更加高效。 可是須要注意的是,不像字典那樣,一個命名元組是不可更改的。好比:
>>> s = Stock('ACME', 100, 123.45)
>>> s
Stock(name='ACME', shares=100, price=123.45)
>>> s.shares = 75
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>>
若是你真的須要改變屬性的值,那麼可使用命名元組實例的 _replace()
方法, 它會建立一個全新的命名元組並將對應的字段用新的值取代。好比:
>>> s = s._replace(shares=75) >>> s Stock(name='ACME', shares=75, price=123.45) >>>
_replace()
方法還有一個頗有用的特性就是當你的命名元組擁有可選或者缺失字段時候, 它是一個很是方便的填充數據的方法。 你能夠先建立一個包含缺省值的原型元組,而後使用 _replace()
方法建立新的值被更新過的實例。好比:
from collections import namedtuple Stock = namedtuple('Stock', ['name', 'shares', 'price', 'date', 'time']) # Create a prototype instance stock_prototype = Stock('', 0, 0.0, None, None) # Function to convert a dictionary to a Stock def dict_to_stock(s): return stock_prototype._replace(**s)
下面是它的使用方法:
>>> a = {'name': 'ACME', 'shares': 100, 'price': 123.45} >>> dict_to_stock(a) Stock(name='ACME', shares=100, price=123.45, date=None, time=None) >>> b = {'name': 'ACME', 'shares': 100, 'price': 123.45, 'date': '12/17/2012'} >>> dict_to_stock(b) Stock(name='ACME', shares=100, price=123.45, date='12/17/2012', time=None) >>>
最後要說的是,若是你的目標是定義一個須要更新不少實例屬性的高效數據結構,那麼命名元組並非你的最佳選擇。 這時候你應該考慮定義一個包含 __slots__
方法的類。
你須要在數據序列上執行彙集函數(好比 sum()
, min()
, max()
), 可是首先你須要先轉換或者過濾數據
一個很是優雅的方式去結合數據計算與轉換就是使用一個生成器表達式參數。 好比,若是你想計算平方和,能夠像下面這樣作:
nums = [1, 2, 3, 4, 5] s = sum(x * x for x in nums)
下面是更多的例子:
# Determine if any .py files exist in a directory
import os files = os.listdir('dirname') if any(name.endswith('.py') for name in files): print('There be python!') else: print('Sorry, no python.') # Output a tuple as CSV s = ('ACME', 50, 123.45) print(','.join(str(x) for x in s)) # Data reduction across fields of a data structure portfolio = [ {'name':'GOOG', 'shares': 50}, {'name':'YHOO', 'shares': 75}, {'name':'AOL', 'shares': 20}, {'name':'SCOX', 'shares': 65} ] min_shares = min(s['shares'] for s in portfolio)
上面的示例向你演示了當生成器表達式做爲一個單獨參數傳遞給函數時候的巧妙語法(你並不須要多加一個括號)。 好比,下面這些語句是等效的:
s = sum((x * x for x in nums)) # 顯示的傳遞一個生成器表達式對象 s = sum(x * x for x in nums) # 更加優雅的實現方式,省略了括號
使用一個生成器表達式做爲參數會比先建立一個臨時列表更加高效和優雅。 好比,若是你不使用生成器表達式的話,你可能會考慮使用下面的實現方式:
nums = [1, 2, 3, 4, 5] s = sum([x * x for x in nums])
這種方式一樣能夠達到想要的效果,可是它會多一個步驟,先建立一個額外的列表。 對於小型列表可能沒什麼關係,可是若是元素數量很是大的時候, 它會建立一個巨大的僅僅被使用一次就被丟棄的臨時數據結構。而生成器方案會以迭代的方式轉換數據,所以更省內存。
在使用一些彙集函數好比 min()
和 max()
的時候你可能更加傾向於使用生成器版本, 它們接受的一個 key 關鍵字參數或許對你頗有幫助。 好比,在上面的證券例子中,你可能會考慮下面的實現版本:
# Original: Returns 20
min_shares = min(s['shares'] for s in portfolio) # Alternative: Returns {'name': 'AOL', 'shares': 20} min_shares = min(portfolio, key=lambda s: s['shares'])
如今有多個字典或者映射,你想將它們從邏輯上合併爲一個單一的映射後執行某些操做, 好比查找值或者檢查某些鍵是否存在。
假如你有以下兩個字典:
a = {'x': 1, 'z': 3 } b = {'y': 2, 'z': 4 }
如今假設你必須在兩個字典中執行查找操做(好比先從 a
中找,若是找不到再在 b
中找)。 一個很是簡單的解決方案就是使用 collections
模塊中的 ChainMap
類。好比:
from collections import ChainMap c = ChainMap(a,b) print(c['x']) # Outputs 1 (from a) print(c['y']) # Outputs 2 (from b) print(c['z']) # Outputs 3 (from a)
一個 ChainMap
接受多個字典並將它們在邏輯上變爲一個字典。 而後,這些字典並非真的合併在一塊兒了, ChainMap
類只是在內部建立了一個容納這些字典的列表 並從新定義了一些常見的字典操做來遍歷這個列表。大部分字典操做都是能夠正常使用的,好比:
>>> len(c) 3 >>> list(c.keys()) ['x', 'y', 'z'] >>> list(c.values()) [1, 2, 3] >>>
若是出現重複鍵,那麼第一次出現的映射值會被返回。 所以,例子程序中的 c['z']
老是會返回字典 a
中對應的值,而不是 b
中對應的值。
對於字典的更新或刪除操做老是影響的是列表中第一個字典。好比:
>>> c['z'] = 10 >>> c['w'] = 40 >>> del c['x'] >>> a {'w': 40, 'z': 10} >>> del c['y'] Traceback (most recent call last): ... KeyError: "Key not found in the first mapping: 'y'" >>>
ChainMap
對於編程語言中的做用範圍變量(好比 globals
, locals
等)是很是有用的。 事實上,有一些方法可使它變得簡單:
>>> values = ChainMap() >>> values['x'] = 1 >>> # Add a new mapping >>> values = values.new_child() >>> values['x'] = 2 >>> # Add a new mapping >>> values = values.new_child() >>> values['x'] = 3 >>> values ChainMap({'x': 3}, {'x': 2}, {'x': 1}) >>> values['x'] 3 >>> # Discard last mapping >>> values = values.parents >>> values['x'] 2 >>> # Discard last mapping >>> values = values.parents >>> values['x'] 1 >>> values ChainMap({'x': 1}) >>>
做爲 ChainMap
的替代,你可能會考慮使用 update()
方法將兩個字典合併。好比:
>>> a = {'x': 1, 'z': 3 } >>> b = {'y': 2, 'z': 4 } >>> merged = dict(b) >>> merged.update(a) >>> merged['x'] 1 >>> merged['y'] 2 >>> merged['z'] 3 >>>
這樣也能行得通,可是它須要你建立一個徹底不一樣的字典對象(或者是破壞現有字典結構)。 同時,若是原字典作了更新,這種改變不會反應到新的合併字典中去。好比:
>>> a['x'] = 13 >>> merged['x'] 1
ChainMap
使用原來的字典,它本身不建立新的字典。因此它並不會產生上面所說的結果,好比:
>>> a = {'x': 1, 'z': 3 } >>> b = {'y': 2, 'z': 4 } >>> merged = ChainMap(a, b) >>> merged['x'] 1 >>> a['x'] = 42 >>> merged['x'] # Notice change to merged dicts 42