我其實一直不太看得明白寫的很簡短的代碼,在讀了幾頁《流暢的Python》後我明白了——是我太菜。周末花了點時間把自己一直不會用的清單推導式和生成器表達式好好看了下,整理一篇筆記,部分代碼是參考了《流暢的Python》中的示例。
在搞明白這倆貨之前,如果需要從兩個資料源取出資料耦合起來,我大機率會把代碼寫成這樣的德行——
for x in xs:
for y in ys:
x_y = (x, y)
嗯,要寫三行,雖然确實很容易看懂意思,但确實不簡潔,再加上經常看到别人寫的代碼裡用到清單推導式和生成器表達式,就更加難受了……
清單推導式,List Comprehension,一般縮寫為listcomps
生成器表達式,Generator Expression,一般縮寫為genexps
掌握這兩個東西,可以讓代碼更簡短,也能讓代碼運作更省記憶體。
List Comprehension
給定一個數字,計算這個數字的平方,然後我們要對一堆數字做這個事情,有兩個辦法——
- 使用lambda函數+map函數
- 使用清單推導式
第一個法子,代碼會寫成這樣
print(
list(map(lambda x: x * x, range(1,11)))
)
我是不喜歡lambda的,總覺得這個寫法很難看,它等同于下面這樣子
def calc(x):
return x * x
print(
list(map(calc, range(1,11)))
)
而且,還得用上map函數,如果你沒用過map函數,比如我就用的少,經常用到的時候想不起來它的兩個參數幹嘛的……
而如果用清單推導式實作這個呢——
print(
[x * x for x in range(1,11)]
)
一行就可以了,也不需要去寫個lambda,或者寫個def去給map調了。對Python來說,lambda和清單推導式的使用原則都差不多——不能太長,可讀性都會變差,就簡短的代碼來說,我更喜歡清單推導式,你可以直接把意思讀出來——
需要x乘以x,x 從 range(1,11) 中循環取出
清單推導式的外部是一個中括号,它提醒你,其永遠是傳回一個清單對象的
上面的幾個寫法的執行結果——
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
對整個清單推導式分成三部分來看——
-
,外殼為中括号,提醒你傳回對象一定是個清單[ list comprehension ]
-
,對循環元素的處理,一個表達式,是以這裡也可以是個函數調用x * x
-
,一個for循環,x實際上是一個局部變量,第三部分直接告訴我們x從哪取,這裡就從for x in range(1,11)
裡取range(1,11)
P.S. Python2的清單推導式是有變量洩漏的問題的,換言之,py2的
x
變量可能會污染外部的同名變量,Python3沒這個問題,Python3的清單推導式有自己的作用域了。
上面的代碼用普通的for循環寫就寫成這樣了——
x_power = []
for x in range(1,11):
x_power.append(x * x)
print(x_power)
嗯,很醜。我以前都這麼寫代碼的。
再下面這個例子來自《流暢的Python一書》
symbols = '!@#$%^&*'
print([ord(symbol) for symbol in symbols])
這裡對每個元素的處理就是調用函數
ord
再看最一開始的兩層循環,實際上就是笛卡爾積,用清單推導式也很好寫
colors = ['Black', 'White', 'Yellow']
sizes = ['XS', 'S', 'M', 'L', 'XL']
t_shirt_models = [(color, size) for color in colors for size in sizes]
print(t_shirt_models)
調整for循環的順序,即可調整實際的循環順序,它和兩層嵌套的循環是等價的
清單推導式裡還可以寫條件,比如下面這樣子
names = ['Addison', 'Lucy', 'Audrey', 'Bella', 'Nova', 'Brooklyn', 'Paisley', 'Savannah',
'Claire', 'Skylar', 'Isla', 'Genesis', 'Naomi', 'Elena', 'Caroline']
names_mt4_from_listcomps = [n for n in names if len(n) > 4]
print(names_mt4_from_listcomps)
條件直接嵌入在for循環後面就好了,我要取出的是名字長度大于4個字元的。
如果用lambda函數+filter函數,就得這麼寫
names_mt4_from_filter = list(filter(lambda n: len(n) > 4, names))
print(names_mt4_from_filter)
嗯,括号都得寫好幾個……
總之,清單推導式看起來就是“順着讀”就好了,是以太長也不合适啦
Generator Expression
生成器表達式寫法和清單推導式幾乎一樣——隻要把中括号換成圓括号就好了,是以,襯衫顔色那個例子就變成這麼寫——
colors = ['Black', 'White', 'Yellow']
sizes = ['XS', 'S', 'M', 'L', 'XL']
t_shirt_models = ((color, size) for color in colors for size in sizes)
print(t_shirt_models)
不過,這個列印出來的就不是完整的結果了,而是——
<generator object <genexpr> at 0x0000020CE2EC4C48>
一個生成器對象。
這是因為生成器遵循疊代器協定,它是一種有“惰性”的東西,你不對它做操作,它就什麼也不做。
是以如果你要處理非常多的資料的時候,資料不需要再複制一次了,資料留在原處就好了,生成器會在“需要”的時候去取資料的,這樣就減少了記憶體的消耗。
理所當然的,你不能在I/O相關的操作裡用它,比如去資料庫讀資料,你要是每次都去讀一次,後果不堪設想……
怎麼讓資料出來呢?簡單的辦法就是for循環咯
for t in t_shirt_models:
print(t)
當然,用一個函數去處理生成器對象也可以,反正它是可以被“疊代”的。
對比listcomps和genexps
打斷點對比可以明顯看出兩者的差别,先來看清單推導式的——
知乎視訊www.zhihu.com
清單推導式會把所有的資料都生成出來,存到記憶體裡,然後才輪到
print()
函數從結果裡去取資料列印。
而生成器表達式,則是每次
print
運作的時候去生成一次資料。
知乎視訊www.zhihu.com
最後,推薦一下《流暢的Python》這本書,在你有了一定的Python基礎之後,一定要看看這本書。我踩了很多書坑,但這本書不坑。