1. 程式人生 > >第一章:Python資料結構和演算法

第一章:Python資料結構和演算法

第一章:Python資料結構和演算法

Python 提供了大量的內建資料結構,包括列表,集合以及字典。大多數情況下使用這些資料結構是很簡單的。 但是,我們也會經常碰到到諸如查詢,排序和過濾等等這些普遍存在的問題。 因此,這一章的目的就是討論這些比較常見的問題和演算法。 另外,我們也會給出在集合模組 collections 當中操作這些資料結構的方法。

1.1 解壓序列賦值給多個變數
1.2 解壓可迭代物件賦值給多個變數
1.3 保留最後 N 個元素
1.4 查詢最大或最小的 N 個元素
1.5 實現一個優先順序佇列
1.6 字典中的鍵對映多個值
1.7 字典排序
1.8 字典的運算
1.9 查詢兩字典的相同點
1.10 刪除序列相同元素並保持順序
1.11 命名切片
1.12 序列中出現次數最多的元素
1.13 通過某個關鍵字排序一個字典列表
1.14 排序不支援原生比較的物件
1.15 通過某個欄位將記錄分組
1.16 過濾序列元素
1.17 從字典中提取子集
1.18 對映名稱到序列元素
1.19 轉換並同時計算資料
1.20 合併多個字典或對映

1.1 解壓序列賦值給多個變數

問題
現在有一個包含 N 個元素的元組或者是序列,怎樣將它裡面的值解壓後同時賦值給 N 個變數?

解決方案
任何的序列(或者是可迭代物件)可以通過一個簡單的賦值語句解壓並賦值給多個變數。 唯一的前提就是變數的數量必須跟序列元素的數量是一樣的。

程式碼示例:
在這裡插入圖片描述
如果變數個數和序列元素的個數不匹配,會產生一個異常。

程式碼示例:
在這裡插入圖片描述
討論
實際上,這種解壓賦值可以用在任何可迭代物件上面,而不僅僅是列表或者元組。 包括字串,檔案物件,迭代器和生成器。

程式碼示例:
在這裡插入圖片描述

有時候,你可能只想解壓一部分,丟棄其他的值。對於這種情況 Python 並沒有提供特殊的語法。 但是你可以使用任意變數名去佔位,到時候丟掉這些變數就行了。

程式碼示例:
在這裡插入圖片描述

1.2 解壓可迭代物件賦值給多個變數

問題
如果一個可迭代物件的元素個數超過變數個數時,會丟擲一個 ValueError 。 那麼怎樣才能從這個可迭代物件中解壓出 N 個元素出來?

解決方案
Python 的星號表示式可以用來解決這個問題。比如,你在學習一門課程,在學期末的時候, 你想統計下家庭作業的平均成績,但是排除掉第一個和最後一個分數。如果只有四個分數,你可能就直接去簡單的手動賦值, 但如果有 24 個呢?這時候星號表示式就派上用場了:
在這裡插入圖片描述
另外一種情況,假設你現在有一些使用者的記錄列表,每條記錄包含一個名字、郵件,接著就是不確定數量的電話號碼。 你可以像下面這樣分解這些記錄:
在這裡插入圖片描述


值得注意的是上面解壓出的 phone_numbers 變數永遠都是列表型別,不管解壓的電話號碼數量是多少(包括 0 個)。 所以,任何使用到 phone_numbers 變數的程式碼就不需要做多餘的型別檢查去確認它是否是列表型別了。

星號表示式也能用在列表的開始部分。比如,你有一個公司前 8 個月銷售資料的序列, 但是你想看下最近一個月資料和前面 7 個月的平均值的對比。你可以這樣做:
在這裡插入圖片描述
下面是在 Python 直譯器中執行的結果:
在這裡插入圖片描述

討論
擴充套件的迭代解壓語法是專門為解壓不確定個數或任意個數元素的可迭代物件而設計的。 通常,這些可迭代物件的元素結構有確定的規則(比如第 1 個元素後面都是電話號碼), 星號表示式讓開發人員可以很容易的利用這些規則來解壓出元素來。 而不是通過一些比較複雜的手段去獲取這些關聯的元素值。

值得注意的是,星號表示式在迭代元素為可變長元組的序列時是很有用的。 比如,下面是一個帶有標籤的元組序列:
在這裡插入圖片描述

星號解壓語法在字串操作的時候也會很有用,比如字串的分割。

程式碼示例:
在這裡插入圖片描述
有時候,你想解壓一些元素後丟棄它們,你不能簡單就使用 * , 但是你可以使用一個普通的廢棄名稱,比如 _ 或者 ign (ignore)。

程式碼示例:
在這裡插入圖片描述
在很多函式式語言中,星號解壓語法跟列表處理有許多相似之處。比如,如果你有一個列表, 你可以很容易的將它分割成前後兩部分:
在這裡插入圖片描述
如果你夠聰明的話,還能用這種分割語法去巧妙的實現遞迴演算法。比如:
在這裡插入圖片描述
然後,由於語言層面的限制,遞歸併不是 Python 擅長的。 因此,最後那個遞迴演示僅僅是個好奇的探索罷了,對這個不要太認真了。

1.3 保留最後 N 個元素

1.3 保留最後 N 個元素
問題
在迭代操作或者其他操作的時候,怎樣只保留最後有限幾個元素的歷史記錄?

解決方案
保留有限歷史記錄正是 collections.deque 大顯身手的時候。比如,下面的程式碼在多行上面做簡單的文字匹配, 並返回匹配所在行的最後N行:
在這裡插入圖片描述

討論
我們在寫查詢元素的程式碼時,通常會使用包含 yield 表示式的生成器函式,也就是我們上面示例程式碼中的那樣。 這樣可以將搜尋過程程式碼和使用搜索結果程式碼解耦。如果你還不清楚什麼是生成器,請參看 4.3 節。

使用 deque(maxlen=N) 建構函式會新建一個固定大小的佇列。當新的元素加入並且這個佇列已滿的時候, 最老的元素會自動被移除掉。

程式碼示例:
在這裡插入圖片描述
儘管你也可以手動在一個列表上實現這一的操作(比如增加、刪除等等)。但是這裡的佇列方案會更加優雅並且執行得更快些。

更一般的, deque 類可以被用在任何你只需要一個簡單佇列資料結構的場合。 如果你不設定最大佇列大小,那麼就會得到一個無限大小佇列,你可以在佇列的兩端執行新增和彈出元素的操作。

程式碼示例:
在這裡插入圖片描述

在佇列兩端插入或刪除元素時間複雜度都是 O(1) ,區別於列表,在列表的開頭插入或刪除元素的時間複雜度為 O(N)

1.4 查詢最大或最小的 N 個元素

問題
怎樣從一個集合中獲得最大或者最小的 N 個元素列表?

解決方案
heapq 模組有兩個函式:nlargest() 和 nsmallest() 可以完美解決這個問題。在這裡插入圖片描述
兩個函式都能接受一個關鍵字引數,用於更復雜的資料結構中:
在這裡插入圖片描述
譯者注:上面程式碼在對每個元素進行對比的時候,會以 price 的值進行比較。

討論
如果你想在一個集合中查詢最小或最大的 N 個元素,並且 N 小於集合元素數量,那麼這些函式提供了很好的效能。 因為在底層實現裡面,首先會先將集合資料進行堆排序後放入一個列表中:
在這裡插入圖片描述
堆資料結構最重要的特徵是 heap[0] 永遠是最小的元素。並且剩餘的元素可以很容易的通過呼叫 heapq.heappop() 方法得到, 該方法會先將第一個元素彈出來,然後用下一個最小的元素來取代被彈出元素(這種操作時間複雜度僅僅是 O(log N),N 是堆大小)。 比如,如果想要查詢最小的 3 個元素,你可以這樣做:
在這裡插入圖片描述
當要查詢的元素個數相對比較小的時候,函式 nlargest() 和 nsmallest() 是很合適的。 如果你僅僅想查詢唯一的最小或最大(N=1)的元素的話,那麼使用 min() 和 max() 函式會更快些。 類似的,如果 N 的大小和集合大小接近的時候,通常先排序這個集合然後再使用切片操作會更快點 ( sorted(items)[:N] 或者是 sorted(items)[-N:] )。 需要在正確場合使用函式 nlargest() 和 nsmallest() 才能發揮它們的優勢 (如果 N 快接近集合大小了,那麼使用排序操作會更好些)。

儘管你沒有必要一定使用這裡的方法,但是堆資料結構的實現是一個很有趣並且值得你深入學習的東西。 基本上只要是資料結構和演算法書籍裡面都會有提及到。 heapq 模組的官方文件裡面也詳細的介紹了堆資料結構底層的實現細節。

1.5 實現一個優先順序佇列

問題
怎樣實現一個按優先順序排序的佇列? 並且在這個佇列上面每次 pop 操作總是返回優先順序最高的那個元素

解決方案
下面的類利用 heapq 模組實現了一個簡單的優先順序佇列:
在這裡插入圖片描述
下面是它的使用方式:
在這裡插入圖片描述
仔細觀察可以發現,第一個 pop() 操作返回優先順序最高的元素。 另外注意到如果兩個有著相同優先順序的元素( foo 和 grok ),pop 操作按照它們被插入到佇列的順序返回的。

討論
這一小節我們主要關注 heapq 模組的使用。 函式 heapq.heappush() 和 heapq.heappop() 分別在佇列 _queue 上插入和刪除第一個元素, 並且佇列 _queue 保證第一個元素擁有最高優先順序( 1.4 節已經討論過這個問題)。 heappop() 函式總是返回”最小的”的元素,這就是保證佇列pop操作返回正確元素的關鍵。 另外,由於 push 和 pop 操作時間複雜度為 O(log N),其中 N 是堆的大小,因此就算是 N 很大的時候它們執行速度也依舊很快。

在上面程式碼中,佇列包含了一個 (-priority, index, item) 的元組。 優先順序為負數的目的是使得元素按照優先順序從高到低排序。 這個跟普通的按優先順序從低到高排序的堆排序恰巧相反。

index 變數的作用是保證同等優先順序元素的正確排序。 通過儲存一個不斷增加的 index 下標變數,可以確保元素按照它們插入的順序排序。 而且, index 變數也在相同優先順序元素比較的時候起到重要作用。

為了闡明這些,先假定 Item 例項是不支援排序的:
在這裡插入圖片描述
如果你使用元組 (priority, item) ,只要兩個元素的優先順序不同就能比較。 但是如果兩個元素優先順序一樣的話,那麼比較操作就會跟之前一樣出錯:
在這裡插入圖片描述
通過引入另外的 index 變數組成三元組 (priority, index, item) ,就能很好的避免上面的錯誤, 因為不可能有兩個元素有相同的 index 值。Python 在做元組比較時候,如果前面的比較已經可以確定結果了, 後面的比較操作就不會發生了:
在這裡插入圖片描述
如果你想在多個執行緒中使用同一個佇列,那麼你需要增加適當的鎖和訊號量機制。 可以檢視 12.3 小節的例子演示是怎樣做的。

heapq 模組的官方文件有更詳細的例子程式以及對於堆理論及其實現的詳細說明。

1.6 字典中的鍵對映多個值

問題
怎樣實現一個鍵對應多個值的字典(也叫 multidict)?

解決方案
一個字典就是一個鍵對應一個單值的對映。如果你想要一個鍵對映多個值,那麼你就需要將這多個值放到另外的容器中, 比如列表或者集合裡面。比如,你可以像下面這樣構造這樣的字典:
在這裡插入圖片描述
選擇使用列表還是集合取決於你的實際需求。如果你想保持元素的插入順序就應該使用列表, 如果想去掉重複元素就使用集合(並且不關心元素的順序問題)。

你可以很方便的使用 collections 模組中的 defaultdict 來構造這樣的字典。 defaultdict 的一個特徵是它會自動初始化每個 key 剛開始對應的值,所以你只需要關注新增元素操作了。比如:
在這裡插入圖片描述
需要注意的是, defaultdict 會自動為將要訪問的鍵(就算目前字典中並不存在這樣的鍵)建立對映實體。 如果你並不需要這樣的特性,你可以在一個普通的字典上使用 setdefault() 方法來代替。比如:
在這裡插入圖片描述
但是很多程式設計師覺得 setdefault() 用起來有點彆扭。因為每次呼叫都得建立一個新的初始值的例項(例子程式中的空列表 [] )。

討論
一般來講,建立一個多值對映字典是很簡單的。但是,如果你選擇自己實現的話,那麼對於值的初始化可能會有點麻煩, 你可能會像下面這樣來實現:
在這裡插入圖片描述
如果使用 defaultdict 的話程式碼就更加簡潔了:
在這裡插入圖片描述
這一小節所討論的問題跟資料處理中的記錄歸類問題有大的關聯。可以參考 1.15 小節的例子。

1.7 字典排序

問題
你想建立一個字典,並且在迭代或序列化這個字典的時候能夠控制元素的順序。

解決方案
為了能控制一個字典中元素的順序,你可以使用 collections 模組中的 OrderedDict 類。 在迭代操作的時候它會保持元素被插入時的順序,示例如下:
在這裡插入圖片描述
當你想要構建一個將來需要序列化或編碼成其他格式的對映的時候, OrderedDict 是非常有用的。 比如,你想精確控制以 JSON 編碼後欄位的順序,你可以先使用 OrderedDict 來構建這樣的資料:
在這裡插入圖片描述
討論
OrderedDict 內部維護著一個根據鍵插入順序排序的雙向連結串列。每次當一個新的元素插入進來的時候, 它會被放到連結串列的尾部。對於一個已經存在的鍵的重複賦值不會改變鍵的順序。

需要注意的是,一個 OrderedDict 的大小是一個普通字典的兩倍,因為它內部維護著另外一個連結串列。 所以如果你要構建一個需要大量 OrderedDict 例項的資料結構的時候(比如讀取 100,000 行 CSV 資料到一個 OrderedDict 列表中去), 那麼你就得仔細權衡一下是否使用 OrderedDict 帶來的好處要大過額外記憶體消耗的影響。

1.8 字典的運算

問題
怎樣在資料字典中執行一些計算操作(比如求最小值、最大值、排序等等)?

解決方案
考慮下面的股票名和價格對映字典:
在這裡插入圖片描述
為了對字典值執行計算操作,通常需要使用 zip() 函式先將鍵和值反轉過來。 比如,下面是查詢最小和最大股票價格和股票值的程式碼:
在這裡插入圖片描述
執行這些計算的時候,需要注意的是 zip() 函式建立的是一個只能訪問一次的迭代器。 比如,下面的程式碼就會產生錯誤:
在這裡插入圖片描述
討論
如果你在一個字典上執行普通的數學運算,你會發現它們僅僅作用於鍵,而不是值。比如:
在這裡插入圖片描述
這個結果並不是你想要的,因為你想要在字典的值集合上執行這些計算。 或許你會嘗試著使用字典的 values() 方法來解決這個問題:
在這裡插入圖片描述
不幸的是,通常這個結果同樣也不是你想要的。 你可能還想要知道對應的鍵的資訊(比如那種股票價格是最低的?)。

你可以在 min() 和 max() 函式中提供 key 函式引數來獲取最小值或最大值對應的鍵的資訊。比如:
在這裡插入圖片描述
但是,如果還想要得到最小值,你又得執行一次查詢操作。比如:
在這裡插入圖片描述

前面的 zip() 函式方案通過將字典”反轉”為 (值,鍵) 元組序列來解決了上述問題。 當比較兩個元組的時候,值會先進行比較,然後才是鍵。 這樣的話你就能通過一條簡單的語句就能很輕鬆的實現在字典上的求最值和排序操作了。

需要注意的是在計算操作中使用到了 (值,鍵) 對。當多個實體擁有相同的值的時候,鍵會決定返回結果。 比如,在執行 min() 和 max() 操作的時候,如果恰巧最小或最大值有重複的,那麼擁有最小或最大鍵的實體會返回:
在這裡插入圖片描述

1.9 查詢兩字典的相同點

問題
怎樣在兩個字典中尋尋找相同點(比如相同的鍵、相同的值等等)?

解決方案
考慮下面兩個字典:
在這裡插入圖片描述
為了尋找兩個字典的相同點,可以簡單的在兩字典的 keys() 或者 items() 方法返回結果上執行集合操作。比如:
在這裡插入圖片描述
這些操作也可以用於修改或者過濾字典元素。 比如,假如你想以現有字典構造一個排除幾個指定鍵的新字典。 下面利用字典推導來實現這樣的需求:
在這裡插入圖片描述
討論
一個字典就是一個鍵集合與值集合的對映關係。 字典的 keys() 方法返回一個展現鍵集合的鍵檢視物件。 鍵檢視的一個很少被瞭解的特性就是它們也支援集合操作,比如集合並、交、差運算。 所以,如果你想對集合的鍵執行一些普通的集合操作,可以直接使用鍵檢視物件而不用先將它們轉換成一個 set。

字典的 items() 方法返回一個包含 (鍵,值) 對的元素檢視物件。 這個物件同樣也支援集合操作,並且可以被用來查詢兩個字典有哪些相同的鍵值對。

儘管字典的 values() 方法也是類似,但是它並不支援這裡介紹的集合操作。 某種程度上是因為值檢視不能保證所有的值互不相同,這樣會導致某些集合操作會出現問題。 不過,如果你硬要在值上面執行這些集合操作的話,你可以先將值集合轉換成 set,然後再執行集合運算就行了。

1.11 命名切片

問題
如果你的程式包含了大量無法直視的硬編碼切片,並且你想清理一下程式碼。

解決方案
假定你要從一個記錄(比如檔案或其他類似格式)中的某些固定位置提取欄位:

0123456789012345678901234567890123456789012345678901234567890’

record = ‘…100 …513.25 …’
cost = int(record[20:23]) * float(record[31:37])

與其那樣寫,為什麼不想這樣命名切片呢:
在這裡插入圖片描述
在這個版本中,你避免了使用大量難以理解的硬編碼下標。這使得你的程式碼更加清晰可讀。
討論
一般來講,程式碼中如果出現大量的硬編碼下標會使得程式碼的可讀性和可維護性大大降低。 比如,如果你回過來看看一年前你寫的程式碼,你會摸著腦袋想那時候自己到底想幹嘛啊。 這是一個很簡單的解決方案,它讓你更加清晰的表達程式碼的目的。

內建的 slice() 函式建立了一個切片物件。所有使用切片的地方都可以使用切片物件。比如:
在這裡插入圖片描述
如果你有一個切片物件a,你可以分別呼叫它的 a.start , a.stop , a.step 屬性來獲取更多的資訊。比如:
在這裡插入圖片描述
另外,你還可以通過呼叫切片的 indices(size) 方法將它對映到一個已知大小的序列上。 這個方法返回一個三元組 (start, stop, step) ,所有的值都會被縮小,直到適合這個已知序列的邊界為止。 這樣,使用的時就不會出現 IndexError 異常。比如:
在這裡插入圖片描述

1.12 序列中出現次數最多的元素

問題
怎樣找出一個序列中出現次數最多的元素呢?

解決方案
collections.Counter 類就是專門為這類問題而設計的, 它甚至有一個有用的 most_common() 方法直接給了你答案。

為了演示,先假設你有一個單詞列表並且想找出哪個單詞出現頻率最高。你可以這樣做:
在這裡插入圖片描述
討論
作為輸入, Counter 物件可以接受任意的由可雜湊(hashable)元素構成的序列物件。 在底層實現上,一個 Counter 物件就是一個字典,將元素對映到它出現的次數上。比如:
在這裡插入圖片描述
如果你想手動增加計數,可以簡單的用加法:
在這裡插入圖片描述
或者你可以使用 update() 方法:
在這裡插入圖片描述
Counter 例項一個鮮為人知的特性是它們可以很容易的跟數學運算操作相結合。比如:
在這裡插入圖片描述
毫無疑問, Counter 物件在幾乎所有需要製表或者計數資料的場合是非常有用的工具。 在解決這類問題的時候你應該優先選擇它,而不是手動的利用字典去實現。

1.13 通過某個關鍵字排序一個字典列表
問題
你有一個字典列表,你想根據某個或某幾個字典欄位來排序這個列表。

解決方案
通過使用 operator 模組的 itemgetter 函式,可以非常容易的排序這樣的資料結構。 假設你從資料庫中檢索出來網站會員資訊列表,並且以下列的資料結構返回:
在這裡插入圖片描述
根據任意的字典欄位來排序輸入結果行是很容易實現的,程式碼示例:
在這裡插入圖片描述
程式碼的輸出如下:
在這裡插入圖片描述
itemgetter() 函式也支援多個 keys,比如下面的程式碼
在這裡插入圖片描述
會產生如下的輸出:
在這裡插入圖片描述

討論
在上面例子中, rows 被傳遞給接受一個關鍵字引數的 sorted() 內建函式。 這個引數是 callable 型別,並且從 rows 中接受一個單一元素,然後返回被用來排序的值。 itemgetter() 函式就是負責建立這個 callable 物件的。

operator.itemgetter() 函式有一個被 rows 中的記錄用來查詢值的索引引數。可以是一個字典鍵名稱, 一個整形值或者任何能夠傳入一個物件的 getitem() 方法的值。 如果你傳入多個索引引數給 itemgetter() ,它生成的 callable 物件會返回一個包含所有元素值的元組, 並且 sorted() 函式會根據這個元組中元素順序去排序。 但你想要同時在幾個欄位上面進行排序(比如通過姓和名來排序,也就是例子中的那樣)的時候這種方法是很有用的。

itemgetter() 有時候也可以用 lambda 表示式代替,比如:
在這裡插入圖片描述
這種方案也不錯。但是,使用 itemgetter() 方式會執行的稍微快點。因此,如果你對效能要求比較高的話就使用 itemgetter() 方式。

最後,不要忘了這節中展示的技術也同樣適用於 min() 和 max() 等函式。比如:
在這裡插入圖片描述

1.14 排序不支援原生比較的物件

問題
你想排序型別相同的物件,但是他們不支援原生的比較操作。

解決方案
內建的 sorted() 函式有一個關鍵字引數 key ,可以傳入一個 callable 物件給它, 這個 callable 物件對每個傳入的物件返回一個值,這個值會被 sorted 用來排序這些物件。 比如,如果你在應用程式裡面有一個 User 例項序列,並且你希望通過他們的 user_id 屬性進行排序, 你可以提供一個以 User 例項作為輸入並輸出對應 user_id 值的 callable 物件。比如:
在這裡插入圖片描述
另外一種方式是使用 operator.attrgetter() 來代替 lambda 函式:
在這裡插入圖片描述
討論
選擇使用 lambda 函式或者是 attrgetter() 可能取決於個人喜好。 但是, attrgetter() 函式通常會執行的快點,並且還能同時允許多個欄位進行比較。 這個跟 operator.itemgetter() 函式作用於字典型別很類似(參考1.13小節)。 例如,如果 User 例項還有一個 first_name 和 last_name 屬性,那麼可以向下面這樣排序:
在這裡插入圖片描述
同樣需要注意的是,這一小節用到的技術同樣適用於像 min() 和 max() 之類的函式。比如:
在這裡插入圖片描述

1.16 過濾序列元素
問題
你有一個數據序列,想利用一些規則從中提取出需要的值或者是縮短序列

解決方案
最簡單的過濾序列元素的方法就是使用列表推導。比如:
在這裡插入圖片描述
使用列表推導的一個潛在缺陷就是如果輸入非常大的時候會產生一個非常大的結果集,佔用大量記憶體。 如果你對記憶體比較敏感,那麼你可以使用生成器表示式迭代產生過濾的元素。比如:在這裡插入圖片描述
有時候,過濾規則比較複雜,不能簡單的在列表推導或者生成器表示式中表達出來。 比如,假設過濾的時候需要處理一些異常或者其他複雜情況。這時候你可以將過濾程式碼放到一個函式中, 然後使用內建的 filter() 函式。示例如下:在這裡插入圖片描述
filter() 函式建立了一個迭代器,因此如果你想得到一個列表的話,就得像示例那樣使用 list() 去轉換。

討論
列表推導和生成器表示式通常情況下是過濾資料最簡單的方式。 其實它們還能在過濾的時候轉換資料。比如:
在這裡插入圖片描述
過濾操作的一個變種就是將不符合條件的值用新的值代替,而不是丟棄它們。 比如,在一列資料中你可能不僅想找到正數,而且還想將不是正數的數替換成指定的數。 通過將過濾條件放到條件表示式中去,可以很容易的解決這個問題,就像這樣:
在這裡插入圖片描述
另外一個值得關注的過濾工具就是 itertools.compress() , 它以一個 iterable 物件和一個相對應的 Boolean 選擇器序列作為輸入引數。 然後輸出 iterable 物件中對應選擇器為 True 的元素。 當你需要用另外一個相關聯的序列來過濾某個序列的時候,這個函式是非常有用的。 比如,假如現在你有下面兩列資料:
在這裡插入圖片描述
現在你想將那些對應 count 值大於5的地址全部輸出,那麼你可以這樣做:
在這裡插入圖片描述
這裡的關鍵點在於先建立一個 Boolean 序列,指示哪些元素符合條件。 然後 compress() 函式根據這個序列去選擇輸出對應位置為 True 的元素。

和 filter() 函式類似, compress() 也是返回的一個迭代器。因此,如果你需要得到一個列表, 那麼你需要使用 list() 來將結果轉換為列表型別。

1.17 從字典中提取子集

問題
你想構造一個字典,它是另外一個字典的子集。

解決方案
最簡單的方式是使用字典推導。比如:
在這裡插入圖片描述
討論
大多數情況下字典推導能做到的,通過建立一個元組序列然後把它傳給 dict() 函式也能實現。比如:
在這裡插入圖片描述
但是,字典推導方式表意更清晰,並且實際上也會執行的更快些 (在這個例子中,實際測試幾乎比 dict() 函式方式快整整一倍)。

有時候完成同一件事會有多種方式。比如,第二個例子程式也可以像這樣重寫:
在這裡插入圖片描述
但是,執行時間測試結果顯示這種方案大概比第一種方案慢 1.6 倍。 如果對程式執行效能要求比較高的話,需要花點時間去做計時測試。 關於更多計時和效能測試,可以參考 14.13 小節。

1.18 對映名稱到序列元素 具名元組

問題
你有一段通過下標訪問列表或者元組中元素的程式碼,但是這樣有時候會使得你的程式碼難以閱讀, 於是你想通過名稱來訪問元素。

解決方案
collections.namedtuple() 函式通過使用一個普通的元組物件來幫你解決這個問題。 這個函式實際上是一個返回 Python 中標準元組型別子類的一個工廠方法。 你需要傳遞一個型別名和你需要的欄位給它,然後它就會返回一個類,你可以初始化這個類,為你定義的欄位傳遞值等。 程式碼示例:
在這裡插入圖片描述
儘管 namedtuple 的例項看起來像一個普通的類例項,但是它跟元組型別是可交換的,支援所有的普通元組操作,比如索引和解壓。 比如:在這裡插入圖片描述
命名元組的一個主要用途是將你的程式碼從下標操作中解脫出來。 因此,如果你從資料庫呼叫中返回了一個很大的元組列表,通過下標去操作其中的元素, 當你在表中添加了新的列的時候你的程式碼可能就會出錯了。但是如果你使用了命名元組,那麼就不會有這樣的顧慮。

為了說明清楚,下面是使用普通元組的程式碼:
在這裡插入圖片描述
下標操作通常會讓程式碼表意不清晰,並且非常依賴記錄的結構。 下面是使用命名元組的版本:
在這裡插入圖片描述
討論
命名元組另一個用途就是作為字典的替代,因為字典儲存需要更多的記憶體空間。 如果你需要構建一個非常大的包含字典的資料結構,那麼使用命名元組會更加高效。 但是需要注意的是,不像字典那樣,一個命名元組是不可更改的。比如:
在這裡插入圖片描述
如果你真的需要改變屬性的值,那麼可以使用命名元組例項的 _replace() 方法, 它會建立一個全新的命名元組並將對應的欄位用新的值取代。比如:
在這裡插入圖片描述
_replace() 方法還有一個很有用的特性就是當你的命名元組擁有可選或者缺失欄位時候, 它是一個非常方便的填充資料的方法。 你可以先建立一個包含預設值的原型元組,然後使用 _replace() 方法建立新的值被更新過的例項。比如:
在這裡插入圖片描述
下面是它的使用方法:
在這裡插入圖片描述
最後要說的是,如果你的目標是定義一個需要更新很多例項屬性的高效資料結構,那麼命名元組並不是你的最佳選擇。 這時候你應該考慮定義一個包含 slots 方法的類(參考8.4小節)。

1.19 轉換並同時計算資料

問題
你需要在資料序列上執行聚集函式(比如 sum() , min() , max() ), 但是首先你需要先轉換或者過濾資料

解決方案
一個非常優雅的方式去結合資料計算與轉換就是使用一個生成器表示式引數。 比如,如果你想計算平方和,可以像下面這樣做:
在這裡插入圖片描述
下面是更多的例子:
在這裡插入圖片描述
討論
上面的示例向你演示了當生成器表示式作為一個單獨引數傳遞給函式時候的巧妙語法(你並不需要多加一個括號)。 比如,下面這些語句是等效的:
在這裡插入圖片描述
使用一個生成器表示式作為引數會比先建立一個臨時列表更加高效和優雅。 比如,如果你不使用生成器表示式的話,你可能會考慮使用下面的實現方式:
在這裡插入圖片描述
這種方式同樣可以達到想要的效果,但是它會多一個步驟,先建立一個額外的列表。 對於小型列表可能沒什麼關係,但是如果元素數量非常大的時候, 它會建立一個巨大的僅僅被使用一次就被丟棄的臨時資料結構。而生成器方案會以迭代的方式轉換資料,因此更省記憶體。

在使用一些聚集函式比如 min() 和 max() 的時候你可能更加傾向於使用生成器版本, 它們接受的一個 key 關鍵字引數或許對你很有幫助。 比如,在上面的證券例子中,你可能會考慮下面的實現版本:
在這裡插入圖片描述

1.20 合併多個字典或對映

問題
現在有多個字典或者對映,你想將它們從邏輯上合併為一個單一的對映後執行某些操作, 比如查詢值或者檢查某些鍵是否存在。

解決方案
假如你有如下兩個字典:
在這裡插入圖片描述
現在假設你必須在兩個字典中執行查詢操作(比如先從 a 中找,如果找不到再在 b 中找)。 一個非常簡單的解決方案就是使用 collections 模組中的 ChainMap 類。比如:
在這裡插入圖片描述
討論
一個 ChainMap 接受多個字典並將它們在邏輯上變為一個字典。 然後,這些字典並不是真的合併在一起了, ChainMap 類只是在內部建立了一個容納這些字典的列表 並重新定義了一些常見的字典操作來遍歷這個列表。大部分字典操作都是可以正常使用的,比如:
在這裡插入圖片描述
如果出現重複鍵,那麼第一次出現的對映值會被返回。 因此,例子程式中的 c[‘z’] 總是會返回字典 a 中對應的值,而不是 b 中對應的值。

對於字典的更新或刪除操作總是影響的是列表中第一個字典。比如:
在這裡插入圖片描述
ChainMap 對於程式語言中的作用範圍變數(比如 globals , locals 等)是非常有用的。 事實上,有一些方法可以使它變得簡單:
在這裡插入圖片描述
作為 ChainMap 的替代,你可能會考慮使用 update() 方法將兩個字典合併。比如:
在這裡插入圖片描述
這樣也能行得通,但是它需要你建立一個完全不同的字典物件(或者是破壞現有字典結構)。 同時,如果原字典做了更新,這種改變不會反應到新的合併字典中去。比如:
在這裡插入圖片描述
ChainMap 使用原來的字典,它自己不建立新的字典。所以它並不會產生上面所說的結果,比如:
在這裡插入圖片描述