Python之父:從列表推導式到生成器表示式

本系列文章譯自Python之父 Guido van Rossum 的系列部落格“The History of Python”。這個部落格系列對我們理解Python及其演變很有幫助,在這裡翻譯推薦給大家,希望大家喜歡,也請大家多多指教!
1. 列表推導式的起源
列表推導式是在Python2.0中新增的特性,最初來自於Greg Ewing提交的patchs,Skip Montanaro和Thomas Wouters也有貢獻(如果我記得沒錯的話,Tim Peter也非常喜歡這個創意)。本質上說,列表推導式是數學中一種“集合”的Python版本。比如說:
{ x | x > 10 }
代表大於10的所有x的集。在數學中,這個表示式代表一個全集(不同語境下,這個集可以有不同的意思,比如所有大於10的實數、所有大於10的整數等)。Python中並沒有一個全集的概念,而在Python2.0中,甚至還沒有集合(元組)的概念(Python中的元組是另一個有趣的故事,會在之後的博文中為大家介紹)。
另外也出於一些其它因素的考慮,就有了Python中的以下表達式:
[ f(x) for x in S if P(x) ]
這個表示式會生成一個列表,由序列 S 中符合 P 條件,並經 f 函式處理得到的值組成。其中條件 P 是可選的, for 迴圈可以巢狀,每一個 for 迴圈都有一個可選的 P 條件(實際上很少用到,因為列表推導式一般用於將一個多維物件轉換成一維列表)。
2. 列表推導式的優勢
列表推導式為Python內建函式 map() 和 filter() 提供了替代方案。
map(f, S) 等價於
[ f(x) for x in S ]
而 filter(P, S) 等價於
[ x for x in S if P(x) ]
有些人可能會覺得,map() 和 filter() 函式似乎更簡潔,完全沒有理由使用列表推導式啊!
然而,在實際工作中並非如此。比如說,如果要為列表中的每個元素加 1 並返回一個新的列表,採用列表推導式寫法是
[ x + 1 for x in S ]
而使用 map() 函式的寫法是
map(lambda x: x+1, S)
其中的 lambda x: x+1
是Python中的匿名函式寫法。
有些人可能會說,就這個例子而言, map() 函式寫法之所以比列表推導式複雜,主要是因為Python中的匿名函式太繁瑣,如果有一個更簡潔的匿名函式寫法,大家就會使用 map() 函數了。
我個人並不同意這種看法——我認為 列表推導式比函式寫法的可讀性要好得多 ,尤其是在處理函式變得更加複雜的情況下。另外, 列表推導式的速度也要比函式表示式快得多 ,因為呼叫 lambda 函式需要建立新的堆疊,而列表推導式不需要。
3. 生成器表示式
由於列表推導式的成功,以及生成器的引入(關於生成器,會在之後的文章中作更多介紹),Python2.4 中新增了一種類似推導式的寫法,用於表示一個處理結果序列,而不用預先生成整個列表,這個新增的特性叫“生成器表示式”。比如說:
sum(x**2 for x in range(1, 11))
這行程式碼呼叫內建的 sum() 函式,使用的引數是一個生成器表示式,生成從 1 到 10 的平方。因此,這個 sum() 函式的意思就是 1 到 10 的平方之和,結果是 385。
在類似例子中,使用生成器表示式的好處很明顯,如果生成列表再求和,這個列表就會在函式執行完成前一直佔用記憶體空間,如果列表比較長,所佔用的記憶體是非常可觀的。
4. 列表推導式與生成器表示式的區別
不得不說,列表推導式與生成器表示式之間的區別非常微妙,比如說,在Python2中,下面這個推導式是成立的:
[ x**2 for x in 1, 2, 3 ]
但在生成器表示式中不成立:
( x**2 for x in 1, 2, 3 )
在生成器表示式中必須這樣寫:
( x**2 for x in (1, 2, 3))
當然,在Python3中,列表推導式也必須要加括號了:
[ x**2 for x in (1, 2, 3) ]
然而,在“一般”或者說“明確”的 for 迴圈中,你依然可以省略括號:
for x in 1, 2, 3: print(x**2)
為什麼推導式和生成器表示式之間要有區別呢?為什麼在 Python3 中列表推導式也使用更嚴格的寫法呢?這兩個問題涉及到的因素包括向後相容、表達歧義、統一寫法以及語言演進等。
在最初版本的 Python 中(未對外發布前 :-),只有語義明確的 for 迴圈寫法,在關鍵字 in 之後使用逗號是沒有歧義的,我想為了省事,當然不寫括號的好。這也讓我想起,在Algol-60語言中,你可以直接寫:
for i := 1, 2, 3 do Statement
並且在這個語言中,你還可以用 step-until 語句替換迴圈物件,比如說:
for i := 1 step 1 until 10, 12 step 2 until 50, 55 step 5 until 100 do Statement
(事後回想,如果Python也能同時迭代多個序列該多好啊,然而……)
當我們在 Python2.0 中增加列表推導式的時候,列表推導式之後只可能有 ] ,或者關鍵字 for 或 if,不會有歧義,因此,不使用括號是沒問題的。
但是當我們在 Python2.4 中增加生成器表示式的時候,就碰到了表達歧義的問題:生成器表示式前後的括號有可能不是生成器表示式的一部分。比如說:
sum(x**2 for x in range(10))
這行程式碼外層的括號是 sum() 函式的一部分,而生成器表示式只是這個函式的第一個引數。因此,有些程式碼可能會有兩種以上的語義,比如說:
sum(x**2 for x in a, b)
有可能可以理解為:
sum(x**2 for x in (a, b))
也可能理解為:
sum((x**2 for x in a), b)
在糾結了很久之後(如果我記得沒錯的話),我們決定,生成器表示式的 in 關鍵字之後必須帶括號。不過當時我們並不想改動(超受歡迎的)列表推導式的寫法。
在之後設計 Python3 的過程中,我們決定,列表推導式的寫法:
[ f(x) for x in S if P(x) ]
應該與使用內建 list() 函式通過生成器表示式生成列表的寫法完全一致:
list(f(x) for x in S if P(x))
因此,列表推導式也被要求使用與生成器表示式一樣的更嚴格的寫法。
5. 變數洩露問題
另外,在Python3中,為了加強列表推導式和生成器表示式寫法之間的統一性,我們還做了一些改動。在Python2中,列表推導式會把內層變數“洩露”到外層中,比如說:
x = 'before' a = [x for x in 1, 2, 3] print x # 這裡會列印 3 而不是 'before'
這是最初版本的列表推導式所帶來的影響,一直以來,都屬於Python的“暗黑小祕密”之一。一開始,這個設計是為了加快列表推導式的運算速度所做的有意妥協。一般來說,新手不會經常踩到這個坑,但不論如何,這個問題還是時不時讓人抓狂。
生成器表示式不會有這個問題,它繼承自生成器,在單獨的棧幀中執行指令。也因為這個原因,生成器表示式(特別用在一個短序列的時候)會比列表推導式效率更低。
在Python3中,我們決定修復這個“暗黑小祕密”,讓列表推導式和生成器表示式採用同樣的繼承策略。因此,在Python3中,因為列表推導式中的 x 不覆蓋外層 x,上面的程式碼會列印 before 而不是 3 。
也不用擔心Python3中列表推導式的執行效率:由於在Python3的整體執行速度方面投入了巨大的努力,不論列表推導式還是生成器表示式,在Python3中都比Python2中執行效率要高!(而且它們在執行效率上也不再有什麼分別)
更新:當然,我忘了提到,Python3還支援元組推導式和字典推導式,它們都是列表推導式這個概念的直接擴充套件。
歡迎關注個人公眾號:讀書錄
