1. 什么是生成器?
介紹生成器之前,我們可以回憶一下,python中函數的調用方式。普通函數調用,函數會立即執行直到函數出現return關鍵字或者執行到最后一行。
明明是生成器,為什么要提到函數呢?這是因為大多數時候生成器是以函數來實現的。
-
普通函數:返回一個值給調用者,把值返回給調用者以后,這個函數就死掉了,也就是被銷毀了。
-
生成器函數:yield(“生出”) 一個值給調用者,yield(“生出”)了一個值以后,函數還活着,調用者有需要的時候會接着生第二個值、第三個值、第四個值。。。
看不懂?沒關系,我們先看看下面一個例子。
編程源於生活:神奇的包子鋪
樓下王大爺開了一件包子鋪,你可不要小瞧這件包子鋪,這件包子鋪有兩個神奇的蒸籠,只要把蒸籠放在蒸架上就能自己產生包子。
小A跟小B同時去吃包子。小B點了50個包子,王大爺就使用神奇的蒸籠一下子給了小B蒸了50個,並且這50個包子使用了50個小碗來裝,裝完以后,王大爺就把蒸架撤下了,於是小B開始坐下吃包子。
小A也買了50個包子,但是他跟王大爺說,你把我的包子放在蒸籠里面,我每次只吃一個。於是王大爺給了小A一個小碗,小A每吃完一個包子,就去蒸籠里面拿一個包子,蒸籠被小A打開的時候,就產生了一個包子給他。
在這里面:
- 小A:生成器函數調用者
- 小B:普通函數的調用者
- 小A的蒸籠:生成器函數(小A拿了一個包子以后,繼續放在蒸架上准備剩下的49個包子,函數保留狀態,可以記錄已經拿了幾個,還剩下幾個)
- 小B的蒸籠:普通函數(給小B拿了50包子,直接就被王大爺收起來了,函數被銷毀,還想吃包子就需要王大爺把蒸籠重新放上蒸架)
- 小A用1個小碗吃:占用內存大小(1KB)
- 小B用50個小碗吃:占用內存大小(50KB)
- 王大爺:CPU
def simple_generator():
x=1
yield
genrator = simple_generator() # 函數內使用yield關鍵字,會返回一個生成器對象
print(type(genrator)) # <class 'generator'>
老樣子,看看生成器對象里面有什么干貨
print(dir(genrator))
[..., '__iter__', '__next__'...] # 又看到了我們的老朋友。。。迭代器里面的兩個兄弟
這里我們能得到什么結論呢?
生成器也是一個迭代器,它具備迭代器的功能。生成器是一個特殊的迭代器
不熟悉迭代器的朋友可以看看我上一期的文章。
2. 創造生成器
2.1 通過yield關鍵字
def simple_generator():
x=1
yield x # 第一次調用next(),執行到這里就停下,返回x
genrator = simple_generator()
print(genrator)
# <generator object simple_generator at 0x7f9e02077660>
print(type(genrator))
# <class 'generator'>
如果一個函數定義中包含yield關鍵字,那么這個函數就不再是一個普通函數,而是一個生成器(generator)
2.2 生成器表達式
generator = (i for i in range(10))
print(generator)
列表推導式的 [ ] 改成 ( )就可以創建一個生成器
那生成器跟列表有什么不同的呢?來舉一個非常直觀的例子
_list = [i for i in range(10)]
print("取出第一個包子:",_list[0]) # 取出第一個包子:0
print("取出第二個包子:",_list[1]) # 取出第二個包子:1
print("取出第三個包子:",_list[2]) # 取出第三個包子:2
for i in _list:
print("取出包子序號:",i)
取出包子序號: 0
取出包子序號: 1
取出包子序號: 2
取出包子序號: 3
取出包子序號: 4
取出包子序號: 5
取出包子序號: 6
取出包子序號: 7
取出包子序號: 8
取出包子序號: 9
generator = (i for i in range(10))
print("取出第一個包子:",next(generator)) # 取出第一個包子:0
print("取出第二個包子:",next(generator)) # 取出第二個包子:1
print("取出第三個包子:",next(generator)) # 取出第三個包子:2
for i in generator:
print("取出包子序號:",i)
取出包子序號: 3 # 這里跟列表有點不一樣,列表每次都從0開始,而生成器只能從當前已經拿到的數開始
取出包子序號: 4
取出包子序號: 5
取出包子序號: 6
取出包子序號: 7
取出包子序號: 8
取出包子序號: 9
對比上面的包子鋪:
小B一次性拿到了50個包子,每個包子放在一個碗里面,假設給碗編號,那么小B可以通過編號任意拿一個包子,小B可以隨意給包子排列組合,小B還喜歡數包子,小B就一直數自己有多少個包子,反反復復數都可以。
但是小A的情況就不一樣了。他只有一個碗,一次只能裝一個,拿了1號包子以后,要想拿2號包子,就只能把1號包子丟掉或者吃掉。小A還不能數包子,他只能記錄自己已經拿了幾個包子
- 小B通過編號任意拿包子:列表索引取值
- 小A拿完1號包子再拿二號:通過next()函數取值
- 小A拿完2號就再也不能拿一號(一號已經被丟掉/吃掉):生成器只能執行一次
- 小B數包子可以重復多次,並且每次都能從1號開始數:
for ... in ...
可以多次,每次都可以從索引為0開始- 小A只能從當前拿到的包子開始數,一旦數完就再也沒法數:
for ... in ...
只能一次,當前拿到第幾個數,就從這個數開始遍歷
結論:生成器保存的是算法,每次調用next(),就計算出下一個元素的值,直到計算到最后一個元素,沒有更多的元素時,拋出StopIteration的錯誤。
3. yield關鍵字
yield這個關鍵字是一個比較抽象的概念。
還是包子鋪:
王大爺把蒸籠放在蒸架上開始蒸包子,小A每次打開蒸籠蓋子,蒸籠會當場捏一個蒸熟了的包子給他,並且自動關上蒸籠蓋。
- 王大爺:CPU
- 蒸籠:生成器函數
- 蒸架:內存空間
- 蒸籠上架:加載函數
- 打開蒸籠蓋子:執行
next()
方法- 將包子給小A:yield(生成)了一個值給調用者
- 關上蒸籠蓋:函數退出(也可以理解為暫停)
- 下一次打開蓋子:又執行
next()
方法,從上一次yield的地方開始執行,遇到下一個yield又退出
def make_baozi(xx):
return xx
def simple_generator():
print("第一次制作豬肉白菜餡的包子")
formulation = "豬肉、白菜"
x_zhu = make_baozi(formulation)
yield x_zhu
# 第一次開蓋子,做好豬肉白菜包子返回給你。暫停,等你吃完,並且記錄我已經把豬肉白菜包子給你了
# 下一次執行上面這塊將不會再執行了,而是從這個關鍵字往后開始執行
print("第二次制作叉燒餡的包子")
formulation = "叉燒"
x_cha = make_baozi(formulation)
yield x_cha # 第二次開蓋子,做好叉燒包子返回給你,暫停,等你吃完,並且記錄我已經把豬肉白菜包子、叉燒包子給你了
print("第三次制作玉米餡的包子")
formulation = "玉米"
x_yu = make_baozi(formulation)
yield x_yu # 第一次開蓋子,做好玉米包子返回給你,暫停,等你吃完,並且記錄我已經把豬肉白菜包子、叉燒包子、玉米包子給你了
genrator = simple_generator()
print(next(genrator))
# 第一次拿豬肉白菜餡的包子
# 豬肉、白菜
print(next(genrator))
# 第二次拿叉燒餡的包子
# 叉燒
print(next(genrator))
# 第三次拿玉米餡的包子
# 玉米
print(next(genrator))
Traceback (most recent call last):
File "/app/util-python/test.py", line 36, in <module>
print(next(genrator))
StopIteration
我們可以把yield
理解成為函數的暫停鍵,next()
函數是開始鍵。
暫停的同時,也會將值返回給你。等下一次開始的時候,就從上一次暫停的地方繼續執行。
3.1 yield from
Python3.3版本的PEP 380中添加了yield from
語法。yield from 可以直接把可迭代對象中的每一個數據作為生成器的結果進行返回
def simple_generator():
a = [1,2,3]
yield a
genrator = simple_generator()
print(genrator.__next__())
# [1,2.3]
def simple_generator():
a = [1,2,3]
yield from a
genrator = simple_generator()
print(genrator.__next__()) # 1
print(genrator.__next__()) # 2
print(genrator.__next__()) # 3
4. 生成器方法
生成器是迭代器的一種,生成器比迭代器多了三種方法:send()
、close()
、throw()
4.1 send
當生成器處於暫停狀態時,向生成器傳一個值
def simple_generator():
a = "測試"
a = yield a
yield a
genrator = simple_generator()
print(genrator.send("dd"))
Traceback (most recent call last):
File "/app/util-python/test.py", line 12, in <module>
genrator.send("dd")
TypeError: can't send non-None value to a just-started generator
上面的用法報錯了,為什么呢?因為此時我們的生成器還沒有啟動,我們需要先啟動生成器。
啟動生成器的方法1:
print(genrator.send(None))
啟動生成器的方法2:
print(genrator.__next__())
生成器啟動后,再嘗試一下:
def simple_generator():
a = "測試"
# 第一次啟動會執行到這里,暫停后,可以通過send向這個關鍵字這里傳參
a = yield a
print("下次執行的代碼塊")
yield a
genrator = simple_generator()
print(genrator.__next__()) # 打印:測試
print(genrator.send("new value")) # 打印:new value
總結一下:這個方法可以向生成器發送一個參數,但是生成器必須先啟動,也就是必須先執行到第一個 yield 關鍵字的地方,然后暫停在這個關鍵字這。此時按下暫停鍵的這個 yield 就可以接受外部send的值的
4.2 throw()
在生成器函數執行暫停處,拋出一個指定的異常
def simple_generator():
a = "開始執行"
try:
yield a
except ValueError:
print("捕獲到了拋進來的異常")
b = "執行第二個yield"
yield b
genrator = simple_generator()
print(genrator.__next__())
# 執行到 yield a 處,所以這里應該是打印:開始執行
print(genrator.throw(ValueError))
# 從 yield a 處往下開始執行,拋出一個 ValueError 異常,如果拋出的異常被處理掉,那么就會接着往下執行到 yield b 處,否則直接拋出異常,程序停止
# 所以此處的結果應該打印:執行第二個yield
可以跟下面的代碼對比這着看看,應該能加深理解
def simple_generator():
a = "開始執行"
try:
yield a
raise ValueError
except ValueError:
print("捕獲到了拋進來的異常")
b = "即將准備執行第二個yield"
yield b
genrator = simple_generator()
print(genrator.__next__())
# 執行到 yield a 處,所以這里應該是打印:開始執行
print(genrator.__next__())
# 從 yield a 處往下開始執行,執行 raise ValueError拋出一個 ValueError 異常,緊接着執行到 yield b 處
4.3 close()
向生成器拋出一個GeneratorExit異常,意味着生成器生命周期結束。
def simple_generator():
a = "開始執行"
try:
yield a
except ValueError:
print("捕獲到了拋進來的異常")
except GeneratorExit:
print("生成器退出")
b = "執行第二個yield"
yield b
genrator = simple_generator()
print(genrator.__next__())
genrator.close()
上面這段代碼最終結果:
Traceback (most recent call last):
File "/app/util-python/test.py", line 23, in <module>
genrator.close()
RuntimeError: generator ignored GeneratorExit
因為生成器已經執行了genrator.close()方法,拋出了了GeneratorExit異常,生成器方法后續執行的語句中,不能再有yield語句,否則會產生 RuntimeError
所以這里需要下面兩行代碼去掉。或者先執行一次 genrator.__next__()
再執行genrator.close()
,讓函數將所有的 yield 執行完再 close
b = "執行第二個yield"
yield b
5. 實現斐波拉契數列(Fibonacci)
def fib(max):
n = 0
a = 0
b = 1
while n < max:
yield b
a, b = b, a+b
n+=1
r = fib(10)
for i in r:
print(i)
1
1
2
3
5
8
13
21
34
55