想理解Python的列表解析嗎?Think in Excel or SQL.

Sam Lin發表於2015-09-19

推導式Python 中最有用的設計之一。它融合了老的、可靠的“map”和“filter” 函式到一段緊湊的程式碼,它語法優雅,允許我們在小段程式碼中表達複雜的想法。推導式是 Python 高手工具箱裡面一個最重要的工具。

然而,我發現很多 Python 程式設計師,包括一些有經驗的開發者,無法完全適應推導式。這有兩個原因:第一,對於什麼時候使用推導式和它們解決哪種問題並不明顯。另外一個同樣重要的原因是,它的語法很難被人記住和理解。

我已經開始在我的 Python 課程 中使用關於推導式的新的解釋和介紹,並且發現這有助於使低年級學生的學習曲線變得不那麼陡峭。在這篇文章中,我將公開我的講解內容,希望有助於 Python 開發者理解什麼時候、在哪裡和如何使用推導式。

舉個簡單的例子:我想輸入一個含有 5 個整數的列表,並且得到含有這 5 個數的平方的列表。如果將這個問題給一個初級(甚至是中級)Python 程式設計師,答案可能會類似這樣:

現在的問題是,這種方法的確奏效。(在我的課堂中,我經常會使用這個短語:“很不幸,這種方法奏效。”)通常,當我討論推導式時,我會討論函式程式設計,不可變資料結構的理念,以及我們不願意改變資料的這一理念。還有在 mapreduce 方面思考的好處。

讓我們忘記上面的東西,並且問你一個更加簡單的問題:如果你將這個問題給你的會計師,他們會如何解決該問題呢?

幾乎可以肯定,一個會計師會開啟 Excel,並且將數字放到同一列:

假設上面的數字位於電子表格的 A 列。Excel 使用者會這樣做,即告訴 Excel B 列應該是 A*A 的計算結果。問題就得到解決了:

你可以說在這裡,不同點是 Excel 有 GUI,而 Python 沒有。但是這不是關鍵點。真正的差異是我們的會計師告訴 Excel 如何將第一列轉化成第二列,Python 開發者則編寫程式來描述如何執行這個轉化。

我們也可以用不同的方式思考這個問題:會計師使用一種並行的方式,將一條表示式應用於一個大型資料集上,而不是序列地解決這個問題,如上面的 for 迴圈。Excel 使用者不關心,甚至不知道,傳遞給表示式的數字的順序。重要的是對於每個數字,只會使用表示式一次,和最後的結果以正確的順序呈現。

我們可能會取笑 Excel,並且視它的使用者為技術新手。當然,很多 Excel 使用者會否認他們擁有高階的程式設計能力。但是這種思維,對於 Excel 使用者是如此的基礎和自然,而對於程式設計師是如此陌生。這很可惜,因為這種思維讓我們用一種簡單的方法表達大量的想法。

總結一下這種方法:

  • 把你的輸入當作可迭代的資料來源
  • 想一下對於資料來源的每個元素,你要使用什麼操作
  • 輸出一個新的序列

這是傳統的 “map” 函式做的事情。Python 的確有一個 “map” 函式,但是今天,我們有代表性地使用列表推導式。

更具體一點,使用我上面使用過的例子:假設我們有一個包含 5 個數字的列表,並且我們想要把那個列表變成它的平方的一個列表。那麼列表推導式的語法看起來會像下面一樣:

呀。難怪大家會被這個語法嚇跑。下面我們把上述語法拆分一下:

  • 首先,這樣將返回一個列表。(這就是為什麼它被叫做“列表推導式”。)那是因為方括號具有強制性,並且會告訴 Python 建立哪種物件。
  • 資料來源是 “range(5)”,它會返回一個列表。
  • 資料來源中的每個元素會依次賦值到可迭代變數 “number” 中。
  • 我們會對資料來源的每個元素都呼叫 “number * number” 運算。

換句話說,我們正建立一個新的列表,該列表的元素是讓資料來源的每個元素都應用了表示式的結果。這聽起來像前面我們的會計師所做的一樣,使用 Excel:我們告訴 Python 我們想要什麼,和如何將源轉化成結果。至於內部是如何工作?如何建立列表?我們不知道也不關心。

列表推導式會讓人感到畏懼而不容易被理解,部分原因是運算的順序似乎不常見。我發現用下面的方法來重寫列表推導式會對理解有所幫助:

沒錯——現在我將列表推導式展開成兩行;第一行描述了我想呼叫的運算,第二行描述了資料來源。如果這似乎還不熟悉,那麼嘗試一下把它放到你可能有經驗的一些場景中:

雖然他們不是直接等效,但是在一個 SQL 的 SELECT 查詢(SELECT 表示式和 FROM 子句的位置)和我們的列表推導式之間有相當多的相似點。一個 SQL 查詢的 FROM 子句描述資料來源,通常會是一個表格,但是也可以是一個檢視或者一個函式呼叫的結果。SELECT 的初始部分通常是列的名字,但是可以包括函式呼叫和運算子。

一方面,SELECT-FROM 組合似乎簡單到不值得一提。因為你僅需從資料來源中獲取一組選擇好的值即可。另一方面,這樣的查詢建立起資料庫的主幹。同樣,這樣的功能建立起很多 Python 程式的主幹,遍歷一個資料結構,取出資料結構的一部分,變換那部分,然後返回一個新的列表。

一個我喜愛的例子(也是我的電子書《Practice Makes Python》中的一個練習)是獲取 Unix 中使用的 /etc/passwd 檔案,然後獲取包含在該檔案中的使用者名稱。/etc/passwd 檔案每行含有一個記錄,欄位用冒號隔開。這是我電腦裡 /etc/passwd 檔案的幾行:

我們通常會將一個檔案看做位元組的集合,當我們閱讀它的時候,我們賦予它語義意義。但是在 Python 裡,我們鼓勵將一個檔案看成一個有序的、可迭代的文字行的集合。沒錯,我可以根據位元組閱讀一個檔案,但是想要閱讀檔案的行是那麼的平常,Python 也提供了一些方法來讀取文字行。

我們知道我們可以遍歷一個檔案的行:

這表明了一個檔案時可迭代的,即意味著它可以充當一個列表推導式的資料來源。這意味著上面的程式碼可以重寫成:

再次,我們的列表推導式的第一行表示我們想要應用到資料來源中每個元素的表示式。在這裡,表示式就是 line。如果我們想要從這些行獲取每一行中的使用者名稱,我們只需使用 string 的 “split” 方法,返回一個列表——然後從結果列表中獲取索引為 0 的值。例如:

再次,我們可以從一個 SQL 查詢的角度來思考它:

當然,上面的“username”是一個列名。對於我的列表推導式,一個更加等效的查詢是帶有“info”列的“Users”表,如下:

注意到在這個例子中,我使用了內建 PostgreSQLsplit_part” 運算子來執行等效於 Python 中 str.split 方法的操作。

記住在我的 SQL 查詢例子中,一個查詢的結果總是看起來和表現得像一個表。返回的列的數量和型別依賴於在 SELECT 語句中表示式的數量和型別。但是結果的集合會有一列或多列,零行或者多行。

同樣,一個列表推導式的結果總是一個列表。在列表推導式裡,你可以擁有任何你想要的表示式;表示式代表列表中的一項,不是列表本身。

例如,假設我想把 /etc/passwd 裡的使用者名稱變成一個字典列表。這裡不需要一個建立單個字典的字典推導式。而是需要一個列表推導式,它的表示式建立一個字典。下面是符合上述內容的一個愚蠢的列表推導式:

上述的程式碼是奏效的,它建立了一個字典列表。每個字典有一對鍵-值對。但是上面的做法似乎有點愚蠢,而我很可能想得到一個包含使用者名稱和數字使用者 ID 的字典,該 ID 處於索引 2 的位置。那麼,我就可以這樣寫:

再次,我們可以從 Excel 的角度去思考,或者是 SQL 的角度:現在,我的查詢產生了一列結果,但是每列包含一個文字字串。我們甚至可以說查詢產生兩列結果,這在 SQL 的世界裡是非常正常的。

請忽略在一個推導式中呼叫兩次 str.split 的效率(或沒有):當我在 Mac 中執行這段程式碼時,它產生一個異常,抱怨一個索引超出了範圍。

原因很簡單:我根據 : 分離每一行,並將分離後的結果放到一個列表中。但是如果有一行沒有包含任何 : 字元,那麼將返回一個單元素列表。因此我需要除去那些不符合的行。特別地,至少在我的 Mac 中,我需要刪除 /etc/passwd 裡面註釋的行,即以‘#’字元開頭的行。

在列表推導式的世界裡,我會像下面這樣寫:

與先前的 SQL 做進一步類比,在 Python 程式碼中新增等效的 SQL 語句註釋:

當然,當推導式首行變得很長的時候,使用函式來代替通常是個好主意。又因為首行可以是任意合法的 Python 表示式,所以使用函式是個好主意:

因此,列表推導式會給你類似一個 SQL SELECT 查詢的能力——除非你不在一個表中查詢資料,而是遵守 Python 的迭代協議,該協議包括大量內建和定製的物件。

那麼,你想要何時使用列表推導式?還有它是如何區別於一個 for 迴圈的?

無論你想何時傳輸資料,使用列表推導式都是合適的。也就是說,你有一個可迭代的資料來源,並且你想建立一個新的列表,該列表的元素是基於資料來源產生的。例如,假設我想找到在 /etc/passwd 中每個字元使用的次數。我可以像下面一樣,使用 collections.Counter

我們知道“counts”是一個列表,因為我使用了一個列表推導式來建立它。它是一個包含很多 Counter 物件的列表,/etc/passwd 中每行非註釋行。如果我想找出在每行中哪個字元出現次數最多將會怎樣?我可以修改表示式,找出 Counter 物件中最常用的字元:

我可以再擴充套件表示式,來獲得每行使用次數最多的字元(在一個單元素列表的雙元素元組裡面):

現在我可以找出每個最常用的字元出現的次數:

在我的電腦中,答案是:

意思是說在 71 行非註釋行中,“:”是最常用的,但是有 4 行最常用的是“e”,有 1 行是“s”。現在,我可以用一個 for 迴圈來完成麼?當然可以——但是因為我正在處理可迭代量,和因為我在這使用適用於這種可迭代量的物件,因此在某種程度上,我可以將它們連線起來,而不需要我告訴 Python 如何做這項工作。我正在做著與會計師一樣的事情,回到這篇文章的開頭——我說出我想要的,讓 Python 做艱苦的工作來為我處理這些事情。

那麼,我應該何時使用 for 迴圈?差別在於你是否想要獲得一個列表,和你是否想執行一條命令很多次。如果你想要構建一個列表,和如果它建立於一個已經存在的可迭代量,那麼我會說列表推導式絕對是最好的選擇。但是如果你想執行某樣東西多次,並且不需要建立一個列表,那麼使用列表推導式是一種糟糕的方法;作為替代,你應該使用一個“for”迴圈。

列表推導式固然比 for 迴圈快。但是很多時候,使用 for 迴圈的場景不同於使用列表推導式的。當你想要將一個可迭代的結構轉化成其它事物時,你不應該使用“for”迴圈;那要用推導式。你也不應該使用列表推導式來執行某項工作(如,print)很多次,即使你可以通過呼叫一個函式來做。何時使用“for”迴圈與何時使用推導式的界限,經驗豐富的 Python 開發者可以在腦海中清晰地描述出來,但是對於新手來說,語言上和這些思想上面都是很模糊的。

所以,總結一下:

  • 如果你想執行一條命令多次,使用“for”迴圈。
  • 如果你有一個可迭代量,並且想建立一個新的可迭代量,那麼列表推導式是你最好的選擇。
  • 構建一個列表推導式有點像在 Excel 的工作:以一組資料開始,然後建立一組新的資料。可以使用任何表示式來將一個事物對映到另一個。你不需要關心 Python 在後臺如何工作;你僅僅想要獲取新的資料。
  • 列表推導式可以由兩部分或者三部分組成,通常,將它們分成幾行會更容易理解:(1)表示式,(2)資料來源,(3)可選的“if”語句。
  • 這三行類似於 SQL 一個查詢中的 SELECT、FROM 和 WHERE 子句。像 SELECT、FROM 和 WHERE 可以使用任意表示式一樣,Python 的列表推導式也可以使用任意表示式。雖然 SELECT 總是返回一組表狀的結果,而列表推導式總是返回一個列表。
  • 你想要建立一個集合,或者可能是一個字典,而不是一個列表嗎?那麼你可以使用集合推導式或者一個字典推導式。這個思想與我講過的列表推導式一樣,除了你的結果會變成一個集合或者一個字典。

你覺得使用列表推導式很難嗎?如果是,使用它們的困難是什麼?上面的內容對你使用列表推導式和記住它們的語法有幫助麼?我很渴望聽到你的迴應,這樣我就可以在未來改善這些解釋了。

打賞支援我翻譯更多好文章,謝謝!

打賞譯者

打賞支援我翻譯更多好文章,謝謝!

想理解Python的列表解析嗎?Think in Excel or SQL.

相關文章