一文帶你搞懂什么是python生成器


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM