Python入門系列34 - 推導(dǎo)式與生成器
Python入門系列34

推導(dǎo)式與生成器
本篇閱讀時間約為 6 分鐘。
1
前言
從本篇開始,進(jìn)入 Python 的技巧篇,介紹下編程時比較 pythonic 的寫法,有些寫法會非常簡潔,比如本文要介紹的推導(dǎo)式。
推導(dǎo)式在各大教程中最常見的是列表推導(dǎo)式,但實際上不僅僅是列表可以進(jìn)行推導(dǎo),集合、字典都有著自己相應(yīng)的推導(dǎo)式。當(dāng)然,像廖雪峰老師寫的教程中,對應(yīng)的叫法是列表生成式,下面讓我們來一一看下。
2
案例需求
老規(guī)矩,依然先給出一個案例的場景,通過此場景來介紹代碼的編寫與實現(xiàn)思路。
案例場景:
現(xiàn)有一個 list ,其中包含著一堆數(shù)字,比如 1- 10。若需要將其中的每個數(shù)字都進(jìn)行平方操作,最終放回 list。此段程序如何編寫呢?
簡單思考一下,看過入門系列之前案例的童鞋們,在沒學(xué)習(xí)列表推導(dǎo)式之前??隙芟氲絻煞N方法。
1. 用 for 循環(huán)遍歷每個元素節(jié)點,在 for 循環(huán)中進(jìn)行平方操作,最后在放回到原有 list 中。
2. 使用 Python 內(nèi)置函數(shù) map 來實現(xiàn),第一個參數(shù)傳入一段計算平方的邏輯函數(shù),第二個參數(shù)傳入原有包含 1-10 的 list 即可。
1 和 2 的實現(xiàn)思路有了,代碼就不在書寫了,忘記的同學(xué)可以去翻看下高階函數(shù)的那篇文章。
下面介紹下,如何使用列表推導(dǎo)式來完成上述需求。
3
列表推導(dǎo)式
代碼實現(xiàn)如下:
# 定義一個包含 1-10 的 list
list_a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 實現(xiàn)列表表達(dá)式,用新的 list 接收下,看的明確一些
list_b = [i*i for i in list_a]
print(list_b)
實際上,說起列表推導(dǎo)式,此處介紹的也只能是 Python 實現(xiàn)的語法而已,Python 解釋器看到此寫法的語法,會將其進(jìn)行相應(yīng)的語法解析。解釋一下,上述代碼中的列表推導(dǎo)式寫法。
既然叫列表推導(dǎo)式,所以 list_b 的定義使用定義 list 的 [] 進(jìn)行定義。在 [] 中的內(nèi)容,可以使用表達(dá)式來書寫,依然使用 for 進(jìn)行對原列表遍歷,與普通的 for 循環(huán)不同的是,簡單計算邏輯,可以直接使用遍歷出來的變量放在 for 關(guān)鍵詞的前面進(jìn)行邏輯處理。
[i*i for i in list_a]????# 列表推導(dǎo)式如果說列表推導(dǎo)式中書寫的是簡單的表達(dá)式,那么自然也可以進(jìn)行一些邏輯判斷。
假設(shè)只想讓原列表中的偶數(shù)進(jìn)行平方計算的邏輯,可以修改代碼如下:
# 定義一個包含 1-10 的 list
list_a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 實現(xiàn)列表表達(dá)式,用新的 list 接收下,看的明確一些
list_b = [i * i for i in list_a if i % 2 == 0]
print(list_b)
語法上來講只需繼續(xù)在 for 循環(huán)后面加上判斷邏輯即可。
4
集合推導(dǎo)式
集合推導(dǎo)式,與列表推導(dǎo)式類似,既然叫集合推導(dǎo)式,那么最終要的結(jié)果必然是以集合形式的語法寫出來的,所以只需要將我們上面推導(dǎo)式地方的代碼,由 list 改為 set 即可。
# 定義一個包含 1-10 的 list
list_a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 實現(xiàn)集合表達(dá)式,用新的變量接收下,看的明確一些
b = {i * i for i in list_a if i % 2 == 0}
print(type(b))
print(b)
集合推導(dǎo)式,修改了定義集合語法的位置,使用 {} 定義,打印結(jié)果變成了 set 。類型也是 set。
5
字典推導(dǎo)式
字典推導(dǎo)式與前面介紹的兩種有些區(qū)別,因為字典是兩個值,key 與 value,所以在寫法上,不能僅僅改變定義,還需要借助字典本身的方法才能實現(xiàn)字典推導(dǎo)式,先看錯誤的寫法,如下:

執(zhí)行一下,提示錯誤,解包的時候有太多值了。
正確的寫法:
# 定義一個字典變量gok(Glory of Kings,王者榮耀的意思)
dict_gok = {
'name': '小妲己',
'age': '18',
'gender': '女'
}
dict_b = {key: vaule for key, vaule in dict_gok.items()}
print(dict_b)
正確的寫法是字典調(diào)用 items方法,將 key - value 取出使用。
小技巧,通過取出時交換 key - value 位置,得到的字典值也相反:

6
生成器generator
你一定會奇怪,為什么沒有元組推導(dǎo)式?那么我們來試試,若按照上面的邏輯來一次元組推導(dǎo)式的寫法會怎么樣呢?
# 定義一個包含 1-10 的 list
list_a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 實現(xiàn)集合表達(dá)式,用新的變量接收下,看的明確一些
b = (i * i for i in list_a if i % 2 == 0)
print(type(b))
print(b)
可以發(fā)現(xiàn),使用“元組定義推導(dǎo)式”,生成出來的是一個 generator 的類型。對于 generator 的使用方法有兩種,一種是直接調(diào)用 next 方法,還有一種通過 for 循環(huán)遍歷使用,推薦使用 for 循環(huán)。

此種方式調(diào)用非得累死,generator 保存的是算法,每次調(diào)用 next ,就計算出下一個元素的值,直到計算到最后一個元素,沒有更多的元素時,還會主動拋出 StopIteration 的錯誤。

所以說,還是 for 循環(huán)最省事:

7
生成器的優(yōu)點
通過列表生成式,我們可以直接創(chuàng)建一個列表。但是,受到內(nèi)存限制,列表容量肯定是有限的。而且,創(chuàng)建一個包含100萬個元素的列表,不僅占用很大的存儲空間,如果我們僅僅需要訪問前面幾個元素,那后面絕大多數(shù)元素占用的空間都白白浪費了。所以,如果列表元素可以按照某種算法推算出來,那我們是否可以在循環(huán)的過程中不斷推算出后續(xù)的元素呢?這樣就不必創(chuàng)建完整的list,從而節(jié)省大量的空間。在Python中,這種一邊循環(huán)一邊計算的機(jī)制,稱為生成器:generator。
廖雪峰的官方網(wǎng)站 - 生成器
在廖雪峰老師官方網(wǎng)站(自行百度查看即可) - 生成器的定義處,其實就很清楚的說明了生成器與列表推導(dǎo)式最大的區(qū)別,這里筆者直接引用過來了,而生成器最大的優(yōu)點就是節(jié)省內(nèi)存,不會因為數(shù)據(jù)龐大而導(dǎo)致內(nèi)存溢出。
當(dāng)然說到生成器,并不僅僅有上面的寫法可以寫出生成器。在函數(shù)中,通過 yield 關(guān)鍵字來代替 return 關(guān)鍵詞,也可以生成對應(yīng)的 generator 生成器。
def test():
for i in range(5):
print('before yield i')
yield i
print('after yield i')
a = test()
print(type(a))
for i in a:
print(i)
與return不同,雖然 yield 會將當(dāng)前值先返回調(diào)用處,但依然會執(zhí)行 yield 后的邏輯,可以看到上面的案例中,返回了 i 后,依然打印了“after yield i”。
8
總結(jié)
列表生成器在編寫代碼時,用得好可以大大簡潔代碼,而生成器有助于大數(shù)據(jù)遍歷時的操作。
筆者早期剛寫代碼時,就遇到了大數(shù)據(jù)讀取到內(nèi)存的問題,那時也是初學(xué) Python ,對于生成器的了解還不是那么深刻,所以沒有利用上,依然清晰地記著項目第一版的代碼,就是暴力的將 3~4 GB 文件內(nèi)容直接讀到內(nèi)存中,最后程序跑著跑著就內(nèi)存溢出了,直接導(dǎo)致程序自己死掉了。
如果當(dāng)時利用好生成器,想必后來也不用那么麻煩的使用“分而治之”的思想去處理文件了。所以當(dāng)遇到大數(shù)據(jù)時,不妨利用生成器試試,尤其是 yield 關(guān)鍵詞,使用得當(dāng)會省去不少麻煩呢!
至此完!

