精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

一文讓你搞懂 Python 的生成器,以及我和一個奇葩之間的恩怨情仇

開發(fā) 前端
關(guān)于協(xié)程的更多細(xì)節(jié),后續(xù)在介紹協(xié)程的時候再說,總之我們現(xiàn)在應(yīng)該使用原生協(xié)程,至于 yield from 就讓它留在歷史的塵埃中吧,我們只需要知道整個演進過程即可。

楔子

本次來聊一聊 Python 的生成器,它是我們后續(xù)理解協(xié)程的基礎(chǔ)(對不起,沒有后續(xù)了)。生成器的話,估計大部分人在寫程序的時候都不怎么用,但其實生成器一旦用好了,確實能給程序帶來性能上的提升,那么下面就來看一看吧。

生成器的基礎(chǔ)知識

我們知道,如果函數(shù)的內(nèi)部出現(xiàn)了 yield 關(guān)鍵字,那么它就不再是普通的函數(shù)了,而是一個生成器函數(shù),調(diào)用之后會返回一個生成器對象。

生成器對象一般用于處理循環(huán)結(jié)構(gòu),應(yīng)用得當(dāng)?shù)脑捒梢詷O大優(yōu)化內(nèi)存使用率。比如:我們讀取一個大文件。

def read_file(file):
    return open(file, encoding="utf-8").readlines()

print(read_file("假裝是大文件.txt"))
"""
['人生是什么?\n', '大概是閃閃發(fā)光的同時\n', '又讓人感到痛苦的東西吧']
"""

這個版本的函數(shù),直接將里面的內(nèi)容全部讀取出來了,返回了一個列表。如果文件非常大,那么內(nèi)存的開銷可想而知。于是我們可以通過 yield 關(guān)鍵字,將普通函數(shù)變成一個生成器函數(shù)。

from typing import Iterator, Generator

def read_file(file):
    with open(file, encoding="utf-8") as f:
        for line in f:
            yield line

data = read_file("假裝是大文件.txt")
# 返回一個生成器對象
print(data)
"""
<generator object read_file at 0x0000019B4FA8BAC0>
"""

# 使用 for 循環(huán)遍歷
for line in data:
    # 文件每一行自帶換行符, 所以這里的 print 就不用換行符了
    print(line, end="")
"""
人生是什么?
大概是閃閃發(fā)光的同時
又讓人感到痛苦的東西吧
"""

由于生成器是一種特殊的迭代器,所以也可以使用它的 __next__ 方法。

def gen():
    yield 123
    yield 456
    yield 789
    return "result"

# 調(diào)用生成器函數(shù)時,會創(chuàng)建一個生成器
# 生成器雖然創(chuàng)建了,但是里面的代碼并沒有執(zhí)行
g = gen()

# 調(diào)用 __next__ 方法時才會執(zhí)行
# 當(dāng)遇到 yield,會將生成器暫停、并返回 yield 后面的值
print(g.__next__())  # 123

# 此時生成器處于暫停狀態(tài),如果我們不驅(qū)動它的話,它是不會前進的
# 再次執(zhí)行 __next__,生成器恢復(fù)執(zhí)行,并在下一個 yield 處暫停
print(g.__next__())  # 456

# 生成器會記住自己的執(zhí)行進度,它總是在遇到 yield 時暫停
# 調(diào)用 __next__ 時恢復(fù)執(zhí)行,直到遇見下一個 yield
print(g.__next__())  # 789

# 顯然再調(diào)用 __next__ 時,已經(jīng)找不到下一個 yield 了
# 那么生成器會拋出 StopIteration,并將返回值設(shè)置在里面
try:
    g.__next__()
except StopIteration as e:
    print(f"返回值:{e.value}")  # 返回值:result

可以看到,基于生成器,我們能夠?qū)崿F(xiàn)惰性求值。

當(dāng)然啦,生成器不僅僅有 __next__ 方法,它還有 send 和 throw 方法,我們先來說一說 send。

def gen():
    res1 = yield "yield 1"
    print(f"***** {res1} *****")
    res2 = yield "yield 2"
    return res2

g = gen()
# 此時程序在第一個 yield 處暫停
print(g.__next__())
"""
yield 1
"""

# 調(diào)用 g.send(val) 依舊可以驅(qū)動生成器執(zhí)行
# 同時還可以傳遞一個值,交給第一個 yield 左邊的 res1
# 然后尋找第二個 yield
print(g.send("嘿嘿"))
"""
***** 嘿嘿 *****
yield 2
"""
# 上面輸出了兩行,第一行是生成器里面的 print 打印的

try:
    # 此時生成器在第二個 yield 處暫停,調(diào)用 g.send 驅(qū)動執(zhí)行
    # 同時傳遞一個值交給第二個 yield 左邊的 res2,然后尋找第三個 yield
    # 但是生成器里面沒有第三個 yield 了,于是拋出 StopIteration
    g.send("蛤蛤")
except StopIteration as e:
    print(f"返回值:{e.value}")
"""
返回值:蛤蛤
"""

生成器永遠(yuǎn)在 yield 處暫停,并將 yield 后面的值返回。如果想驅(qū)動生成器繼續(xù)執(zhí)行,可以調(diào)用 __next__ 或 send,會去尋找下一個 yield,然后在下一個 yield 處暫停。依次往復(fù),直到找不到 yield 時,拋出 StopIteration,并將返回值包在里面。

但是這兩者的不同之處在于,send 可以接收參數(shù),假設(shè)生成器在 res = yield 123 這里停下來了。

當(dāng)調(diào)用 __next__ 和 send 的時候,都可以驅(qū)動執(zhí)行,但調(diào)用 send 時可以傳遞一個 value,并將 value 賦值給變量 res。而 __next__ 沒有這個功能,如果是調(diào)用 __next__ 的話,那么 res 得到的就是一個 None。

所以 res = yield 123 這一行語句需要兩次驅(qū)動生成器才能完成,第一次驅(qū)動會讓生成器執(zhí)行到 yield 123,然后暫停執(zhí)行,將 123 返回。第二次驅(qū)動才會給變量 res 賦值,此時會尋找下一個 yield 然后暫停。

生成器的預(yù)激

剛創(chuàng)建生成器的時候,里面的代碼還沒有執(zhí)行,它的 f_lasti 是 -1。關(guān)于什么是 f_lasti,需要解釋一下。

首先隨著 CPython 版本的升級,一些數(shù)據(jù)結(jié)構(gòu)的底層實現(xiàn)也在發(fā)生改變,比如棧幀等等。在之前的版本中,棧幀有一個字段叫 f_lasti,它表示最近一條執(zhí)行完畢的字節(jié)碼指令的偏移量。而在 3.12 里面,這個字段已經(jīng)沒了。

雖然解釋器內(nèi)部結(jié)構(gòu)會發(fā)生變化,但暴露出來的 Python 接口是不變的,所以我們依舊可以訪問該字段。

def gen():
    res1 = yield 123
    res2 = yield 456
    return "result"

g = gen()
# 生成器函數(shù)和普通函數(shù)一樣,執(zhí)行時也會創(chuàng)建棧幀
# 通過 g.gi_frame 可以很方便的獲取
print(g.gi_frame.f_lasti)  # -1

f_lasti 是 -1,表示生成器剛被創(chuàng)建,還沒有執(zhí)行任何指令。而第一次驅(qū)動生成器執(zhí)行,叫做生成器的預(yù)激。但在生成器還沒有被預(yù)激時,我們調(diào)用 send,里面只能傳遞一個 None,否則報錯。

def gen():
    res1 = yield 123
    res2 = yield 456
    return "result"

g = gen()
try:
    g.send("小云同學(xué)")
except TypeError as e:
    print(e)
"""
can't send non-None value to a just-started generator
"""

對于尚未被預(yù)激的生成器,我們只能傳遞一個 None,也就是 g.send(None)。或者調(diào)用 g.__next__(),因為不管何時它傳遞的都是 None。

其實也很好理解,我們之所以傳值是為了賦給 yield 左邊的變量,這就意味著生成器必須至少被驅(qū)動一次、在某個 yield 處停下來才可以。而未被預(yù)激的生成器,它里面的代碼壓根就沒有執(zhí)行,所以第一次驅(qū)動的時候只能傳遞一個 None 進去。

如果查看生成器的源代碼的話,也能證明這一點:

圖片圖片

在之前的版本中,判斷條件是 f_lasti 是否等于 -1,而在 3.12 中引入了 gi_frame_state 字段,表示生成器的狀態(tài)。如果生成器剛創(chuàng)建,并且接收的參數(shù) arg 不為 None,那么報錯。

那么生成器的狀態(tài)都有哪些呢?

// Include/internal/pycore_frame.h
typedef enum _framestate {
    FRAME_CREATED = -2,
    FRAME_SUSPENDED = -1,
    FRAME_EXECUTING = 0,
    FRAME_COMPLETED = 1,
    FRAME_CLEARED = 4
} PyFrameState;

狀態(tài)總共有五種。

  • FRAME_CREATED:生成器剛創(chuàng)建。
  • FRAME_SUSPENDED:生成器被掛起,也就是執(zhí)行到某個 yield 之后返回了。
  • FRAME_EXECUTING:生成器執(zhí)行中。
  • FRAME_COMPLETED:生成器執(zhí)行完畢,但棧幀對象還未被清理。
  • FRAME_CLEARED:生成器的棧幀對象被清理。

相關(guān)源碼細(xì)節(jié)下一篇文章(對不起,沒有下一篇了)會分析。

生成器的 throw 方法

除了 __next__ 和 send 方法之外,生成器還有一個 throw 方法,該方法的作用和前兩者類似,也是驅(qū)動生成器執(zhí)行,并在下一個 yield 處暫停。但它在調(diào)用的時候,需要傳遞一個異常進去。

def gen():
    try:
        yield 123
    except ValueError as e:
        print(f"異常:{e}")
    yield 456
    return "result"

g = gen()
# 生成器在 yield 123 處暫停
g.__next__()
# 向生成器傳遞一個異常
# 如果當(dāng)前生成器的暫停位置處無法捕獲傳遞的異常,那么會將異常拋出來
# 如果能夠捕獲,那么會驅(qū)動生成器執(zhí)行,并在下一個 yield 處暫停
# 當(dāng)前生成器位于 yield 123 處,而它所在的位置能夠捕獲異常
# 所以不會報錯,結(jié)果就是 456 會賦值給 val
val = g.throw(ValueError("一個 ValueError"))
"""
異常:一個 ValueError
"""
print(val)
"""
456
"""

關(guān)于生成器的 __next__、send、throw 三個方法的用法我們就說完了,還是比較簡單的。

關(guān)閉生成器

生成器也是可以關(guān)閉的,我們來看一下。

def gen():
    yield 123
    yield 456
    return "result"

g = gen()
# 生成器在 yield 123 處停止
print(g.__next__())  # 123
# 關(guān)閉生成器
g.close()
# 生成器一旦關(guān)閉,就代表執(zhí)行完畢了,它的棧幀會被重置為 None
print(g.gi_frame)  # None
try:
    # 再次調(diào)用 __next__,會拋出 StopIteration
    g.__next__()
except StopIteration as e:
    # 此時 e.value 為 None
    print(e.value)  # None

無論是顯式地關(guān)閉生成器,還是正常情況下生成器執(zhí)行完畢,內(nèi)部的棧幀都會被重置為 None。而驅(qū)動一個已經(jīng)執(zhí)行結(jié)束的生成器,會拋出 StopIteration 異常,并且異常的 value 屬性為 None。

GeneratorExit 異常

這里再來說一說 GeneratorExit 這個異常,如果我們關(guān)閉一個生成器(或者生成器被刪除時),那么會往里面扔一個 GeneratorExit 進去。

def gen():
    try:
        yield 123
    except GeneratorExit as e:
        print("生成器被刪除了")

g = gen()
# 生成器在 yield 123 處暫停
g.__next__()
# 關(guān)閉生成器,會往里面扔一個 GeneratorExit
g.close()
"""
生成器被刪除了
"""

這里我們捕獲了傳遞的 GeneratorExit,所以 print 語句執(zhí)行了,但如果沒有捕獲呢?

def gen():
    yield 123

g = gen()
g.__next__()
g.close()

此時無事發(fā)生,但是注意:如果是手動調(diào)用 throw 方法扔一個 GeneratorExit 進去,異常還是會拋出來的。

那么問題來了,生成器為什么要提供這樣一種機制呢?直接刪就完了,干嘛還要往生成器內(nèi)部丟一個異常呢?答案是為了資源的清理和釋放。

在 Python 還未提供原生協(xié)程,以及 asyncio 還尚未流行起來的時候,很多開源的協(xié)程框架都是基于生成器實現(xiàn)的協(xié)程。而創(chuàng)建連接的邏輯,一般都會寫在 yield 后面。

def _create_connection():
    # 一些邏輯
    yield conn
    # 一些邏輯

但是這些連接在不用的時候,要不要進行釋放呢?答案是肯定的,所以便可以這么做。

def _create_connection():
    # 一些邏輯
    try: 
        yield conn
    except GeneratorExit:
        conn.close()
    # 一些邏輯

這樣當(dāng)我們關(guān)閉或刪除生成器的時候,就能夠自動對連接進行釋放了。

不過還有一個需要注意的點,就是在捕獲 GeneratorExit 之后,不可以再執(zhí)行 yield,否則會拋出 RuntimeError。

def gen():
    try:
        yield 123
    except GeneratorExit:
        print("生成器被刪除")
        yield

g = gen()
g.__next__()
g.close()
"""
生成器被刪除
Traceback (most recent call last):
  File "...", line 10, in <module>
    g.close()
RuntimeError: generator ignored GeneratorExit
"""

調(diào)用 close 方法時,如果沒有成功捕獲 GeneratorExit,那么生成器會直接關(guān)閉,不會有任何事情發(fā)生。但如果捕獲了 GeneratorExit,那么可以在對應(yīng)的語句塊里做一些資源清理邏輯,但不應(yīng)該再出現(xiàn) yield。

而上面的例子中出現(xiàn)了 yield,所以解釋器會拋出 RuntimeError,因為沒捕獲 GeneratorExit 還好,解釋器不會有什么抱怨。但如果捕獲了 GeneratorExit,說明我們知道生成器是被關(guān)閉了,既然知道,那里面還出現(xiàn) yield 的意義何在呢?

當(dāng)然啦,如果出現(xiàn)了 yield,但沒有執(zhí)行到,則不會拋 RuntimeError。

def gen():
    try:
        yield 123
    except GeneratorExit:
        print("生成器被刪除")
        return
        yield

g = gen()
g.__next__()
g.close()
print("------------")
"""
生成器被刪除
------------
"""

遇見 yield 之前就返回了,所以此時不會出現(xiàn) RuntimeError。

注意:GeneratorExit 繼承自 BaseException,它無法被 Exception 捕獲。

yield from 的用法

當(dāng)函數(shù)內(nèi)部出現(xiàn)了 yield 關(guān)鍵字,那么它就是一個生成器函數(shù),對于 yield from 而言亦是如此。那么問題來了,這兩者之間有什么區(qū)別呢?

from typing import Generator

def gen1():
    yield [1, 2, 3]

def gen2():
    yield from [1, 2, 3]

g1 = gen1()
g2 = gen2()
# 兩者都是生成器
print(isinstance(g1, Generator))  # True
print(isinstance(g2, Generator))  # True

print(g1.__next__())  # [1, 2, 3]
print(g2.__next__())  # 1

結(jié)論很清晰,yield 對后面的值沒有要求,會直接將其返回。而 yield from 后面必須跟一個可迭代對象(否則報錯),然后每次返回可迭代對象的一個值。

def gen():
    yield from [1, 2, 3]
    return "result"

g = gen()
print(g.__next__())  # 1
print(g.__next__())  # 2
print(g.__next__())  # 3
try:
    g.__next__()
except StopIteration as e:
    print(e.value)  # result

除了要求必須跟一個可迭代對象,然后每次只返回一個值之外,其它表現(xiàn)和 yield 是類似的。而對于當(dāng)前這個例子來說,yield from [1, 2, 3] 等價于 for item in [1, 2, 3]: yield item。

所以有人覺得 yield from 貌似沒啥用啊,它完全可以用 for 循環(huán)加 yield 進行代替。很明顯不是這樣的,yield from 背后做了非常多的事情,我們稍后說。

這里先出一道思考題:

圖片圖片

這時候便可以通過 yield 和 yield from 來實現(xiàn)這一點。

def flatten(data):
    for item in data:
        if isinstance(item, list):
            yield from flatten(item)
        else:
            yield item


data = [1, [[[[[3, 3], 5]]], [[[[[[[[[[[[6]]]]], 8]]], "aaa"]]]], 250]]
print(list(flatten(data)))  # [1, 3, 3, 5, 6, 8, 'aaa', 250]

怎么樣,是不是很簡單呢?

委托生成器

如果單從語法上來看的話,會發(fā)現(xiàn) yield from 貌似沒什么特殊的地方,但其實 yield from 還可以作為委托生成器。委托生成器會在調(diào)用方和子生成器之間建立一個雙向通道,什么意思呢?我們舉例說明。

def gen():
    yield 123
    yield 456
    return "result"

def middle():
    res = yield from gen()
    print(f"接收到子生成器的返回值: {res}")

# middle 里面出現(xiàn)了 yield from gen()
# 此時 middle() 便是委托生成器,gen() 是子生成器
g = middle()

# 而 yield from 會在調(diào)用方和子生成器之間建立一個雙向通道
# 兩者是可以互通的,調(diào)用 g.send、g.throw 都會直接傳遞給子生成器
print(g.__next__())  # 123
print(g.__next__())  # 456

# 問題來了,如果再調(diào)用一次 __next__ 會有什么后果呢?
# 按照之前的理解,應(yīng)該會拋出 StopIteration
print(g.__next__())
"""
接收到子生成器的返回值: result
Traceback (most recent call last):
  File "...", line 21, in <module>
    print(g.__next__())
StopIteration
"""

在第三次調(diào)用 __next__ 的時候,確實拋了異常,但是委托生成器收到了子生成器的返回值。也就是說,委托生成器在調(diào)用方和子生成器之間建立了雙向通道,兩者是直接通信的,并且當(dāng)子生成器出現(xiàn) StopIteration 時,委托生成器還要負(fù)責(zé)兜底。

委托生成器會將子生成器拋出的 StopIteration 里面的 value 取出來,然后賦值給左側(cè)的變量 res,并在自己內(nèi)部繼續(xù)尋找 yield。

換句話說,當(dāng)子生成器 return 之后,委托生成器會拿到返回值,并將子生成器拋出的異常給捕獲掉。但是還沒完,因為還要找到下一個 yield,那么從哪里找呢?顯然是從委托生成器的內(nèi)部尋找,于是接下來就變成了調(diào)用方和委托生成器之間的通信。

如果在委托生成器內(nèi)部能找到下一個 yield,那么會將值返回給調(diào)用方。如果找不到,那么就重新構(gòu)造一個 StopIteration,將異常拋出去。此時異常的 value 屬性,就是委托生成器的返回值。

def gen():
    yield 123
    return "result"

def middle():
    res = yield from gen()
    return f"委托生成器返回了子生成器的返回值:{res}"

g = middle()
print(g.__next__())  # 123
try:
    g.__next__()
except StopIteration as e:
    print(e.value)  # 委托生成器返回了子生成器的返回值:result

大部分情況下,我們并不關(guān)注委托生成器的返回值,我們更關(guān)注的是子生成器。于是可以換種寫法:

def gen():
    yield 123
    yield 456
    yield 789
    return "result"

def middle():
    yield (yield from gen())

g = middle()
for v in g:
    print(v)
"""
123
456
789
result
"""

所以委托生成器負(fù)責(zé)在調(diào)用方和子生成器之間建立一個雙向通道,通道一旦建立,調(diào)用方可以和子生成器直接通信。雖然調(diào)用的是委托生成器的 __next__、send、throw 等方法,但影響的都是子生成器。

并且委托生成器還可以對子生成器拋出的 StopIteration 異常進行兜底,會捕獲掉該異常,然后拿到返回值,這樣就無需手動捕獲子生成器的異常了。但問題是委托生成器還要找到下一個 yield,并將值返回給調(diào)用方,此時這個重?fù)?dān)就落在了它自己頭上。

如果找不到,還是要將異常拋出來的,只不過拋出的 StopIteration 是委托生成器構(gòu)建的。而子生成器拋出的 StopIteration,早就被委托生成器捕獲掉了。于是我們可以考慮在 yield from 的前面再加上一個 yield,這樣就不會拋異常了。

為什么要有委托生成器

我們上面已經(jīng)了解了委托生成器的用法,不過問題來了,這玩意為啥會存在呢?上面的邏輯,即便不使用 yield from 也可以完成啊。

其實是因為我們上面的示例代碼比較簡單(為了演示用法),當(dāng)需求比較復(fù)雜時,將生成器內(nèi)部的部分操作委托給另一個生成器是有必要的,這也是委托生成器的由來。

而委托生成器不僅要能保證調(diào)用方和子生成器之間直接通信,還要能夠以一種優(yōu)雅的方式獲取子生成器的返回值,于是新的語法 yield from 就誕生了。

但其實 yield from 背后為我們做得事情還不止這么簡單,它不單單是建立雙向通道、獲取子生成器的返回值,它還會處理子生成器內(nèi)部出現(xiàn)的異常,詳細(xì)內(nèi)容可以查看 PEP380。

https://peps.python.org/pep-0380/

這里我們直接給出結(jié)論,并通過代碼演示一下。

1)子生成器 yield 后面的值,會直接返回給調(diào)用方;調(diào)用方 send 發(fā)送的值,也會直接傳給子生成器。

def gen():
    res = yield 123
    yield [res]
    return "result"

def middle():
    yield (yield from gen())

g = middle()
# 子生成器 yield 后面的值,會直接返回給調(diào)用方
print(g.__next__())  # 123
# 調(diào)用方 send 發(fā)送的值,也會直接傳給子生成器
print(g.send("小云同學(xué)"))  # ['小云同學(xué)']

另外還要補充一個細(xì)節(jié),如果 yield from 一個已經(jīng)消耗完畢的生成器,會直接返回 None。

def gen():
    yield 123
    return "result"

def middle():
    sub = gen()
    res = yield from sub
    yield res + " from gen()"
    # 到這里的話,sub = gen() 這個生成器已經(jīng)被消耗完畢了
    # 如果我們繼續(xù) yield from 的話,會直接返回 None
    res = yield from sub
    yield f"res: {res}"

g = middle()
print(g.__next__())  # 123
print(g.__next__())  # result from gen()
# 此處執(zhí)行 g.__next__() 時
# 委托生成器內(nèi)部會執(zhí)行第二個 res = yield from sub
# 但問題是 sub 之前就已經(jīng)被消耗完了,所以會直接返回 None,然后尋找下一個 yield
print(g.__next__())  # res: None

所以不要對生成器做二次消費。

2)子生成器結(jié)束時,最后的 return value 等價于 raise StopIteration(value)。然后該異常會被 yield from 捕獲,并將 value 賦值給 yield from 左側(cè)的變量。并且在拿到子生成器的返回值時,委托生成器會繼續(xù)運行,尋找下一個 yield。

def gen():
    yield 123
    return "result"

def middle():
    res = yield from gen()
    yield res + " from middle()"

g = middle()
print(g.__next__())  # 123
# 子生成器 gen() 在 return 時會拋出 StopIteration
# 然后在委托生成器內(nèi)部被捕獲,并將返回值賦給 res
# 接著繼續(xù)尋找下一個 yield
print(g.__next__())  # result from middle()

另外補充一點,生成器在 return 時,等價于拋出一個 StopIteration。但異常必須在 return 的時候隱式拋出,如果是在生成器內(nèi)部 raise StopIteration 則是不合法的。

def gen():
    yield 123
    raise StopIteration("result")

g = gen()
print(g.__next__())  # 123
print(g.__next__())
"""
Traceback (most recent call last):
  File "......", line 3, in gen
    raise StopIteration("result")
StopIteration: result

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "......", line 7, in <module>
    print(g.__next__())
RuntimeError: generator raised StopIteration
"""

此時會引發(fā)一個 RuntimeError。

3)如果子生成器在執(zhí)行的過程中,內(nèi)部出現(xiàn)了異常,那么會將異常丟給委托生成器。委托生成器會嘗試處理該異常,如果處理不了,那么再調(diào)用子生成器的 throw 方法將異常扔回去。

def gen():
    yield 123
    raise ValueError("出了個錯")
    return "result"

def middle():
    yield from gen()

g = middle()
print(g.__next__())  # 123
# 此時子生成器會拋出 ValueError,而委托生成器沒有異常捕獲邏輯,無法處理
# 于是會調(diào)用子生成器的 throw 方法,將異常重新扔回去,最終由調(diào)用方來處理
try:
    print(g.__next__())  # 123
except ValueError as e:
    print(e)  # 出了個錯

那如果委托生成器可以處理子生成器拋出的異常呢?

def gen():
    yield 123
    raise ValueError("出了個錯")
    return "result"

def middle():
    try:
        yield from gen()
    except ValueError as e:
        yield f"異常:{e}"
    # 當(dāng)子生成器拋出異常時,它就已經(jīng)結(jié)束了
    yield "result from middle()"

g = middle()
print(g.__next__())  # 123
print(g.__next__())  # 異常:出了個錯
print(g.__next__())  # result from middle()

如果委托生成器可以處理子生成器拋出的異常,那么接下來就是調(diào)用方和委托生成器之間的事情了。

再比如我們將生成器 close 掉,看看結(jié)果會怎樣,我們知道它會 throw 一個 GeneratorExit。

def gen():
    yield 123
    return "result"

def middle():
    try:
        yield from gen()
    except GeneratorExit as e:
        print(f"子生成器結(jié)束了")

g = middle()
print(g.__next__())  # 123
# 關(guān)閉子生成器,會 throw 一個 GeneratorExit
# 然后這個 GeneratorExit 會向上透傳給委托生成器
g.close()
"""
子生成器結(jié)束了
"""
# 注意:委托生成器也是同理
# 一旦捕獲了 GeneratorExit,后續(xù)不應(yīng)該再出現(xiàn) yield

yield from 算是 Python 里面特別難懂的一個語法了,但如果理解了 yield from,后續(xù)理解 await 就會簡單很多。

生成器表達式

Python 里面還有一個生成器表達式,我們來看一下。

from typing import Generator

g = (x for x in range(10))
print(isinstance(g, Generator))  # True
print(g)  # <generator object <genexpr> at 0x...>

print(g.__next__())  # 0
print(g.__next__())  # 1

如果表達式是在一個函數(shù)里面,那么生成器表達式周圍的小括號可以省略掉。

import random

d = [random.randint(1, 10) for _ in range(100)]
# 我們想統(tǒng)計里面大于 5 的元素的總和
# 下面兩種做法都是可以的
print(
    sum((x for x in d if x > 5)),
    sum(x for x in d if x > 5)
)  # 397 397

這兩種做法是等價的,字節(jié)碼完全一樣。

但要注意,生成器表達式還存在一些陷阱,一不小心就可能踩進去。至于是什么陷阱呢?很簡單,一句話:使用生成器表達式創(chuàng)建生成器的時候,in 后面的變量就已經(jīng)確定了,但其它的變量則不會。舉個栗子:

g = (巭孬嫑夯烎 for x in [1, 2, 3])

執(zhí)行這段代碼不會報錯,盡管 for 前面那一坨我們沒有定義,但不要緊,因為生成器是惰性執(zhí)行的。但如果我們調(diào)用了 g.__next__(),那么很明顯就會報錯了,會拋出 NameError。

g = (x for x in lst)

但是這段代碼會報錯:NameError: name 'lst' is not defined,因為 in 后面的變量在創(chuàng)建生成器的時候就已經(jīng)確定好了。而在創(chuàng)建生成器的時候,發(fā)現(xiàn) lst 沒有定義,于是拋出 NameError。

所以,陷阱就來了:

i = 1
g = (x + i for x in [1, 2, 3])
i = 10
# 輸出的不是 (2, 3, 4)
print(tuple(g))  # (11, 12, 13)

因為生成器只有在執(zhí)行的時候,才會去確定變量 i 究竟指向誰,而調(diào)用 tuple(g) 的時候 i 已經(jīng)被修改了。

lst = [1, 2, 3]
g = (x for x in lst)
lst = [4, 5, 6]
print(tuple(g))  # (1, 2, 3)

但這里輸出的又是 (1, 2, 3),因為在創(chuàng)建生成器的時候,in 后面的變量就已經(jīng)確定了,這里會和 lst 指向同一個列表。而第三行改變的只是變量 lst 的指向,和生成器無關(guān)。

g = (x for x in [1, 2, 3, 4])
for i in [1, 10]:
    g = (x + i for x in g)

print(tuple(g))

思考一下,上面代碼會打印啥?下面進行分析:

  • 初始的 g,可以看成是 (1, 2, 3, 4),因為 in 后面是啥,在創(chuàng)建生成器的時候就確定了;
  • 第一次循環(huán)之后,g 就相當(dāng)于 (1+i, 2+i, 3+i, 4+i);
  • 第二次循環(huán)之后,g 就相當(dāng)于 (1+i+i, 2+i+i, 3+i+i, 4+i+i);

而循環(huán)結(jié)束后,變量 i 會指向 10,所以打印結(jié)果就是 (21, 22, 23, 24)。

生成器與協(xié)程

在 Python 還沒有引入原生協(xié)程的時候,很多開源框架都是基于生成器模擬的協(xié)程,最經(jīng)典的莫過于 Tornado。然而事實上,即便是原生協(xié)程,在底層也是基于生成器實現(xiàn)的。

async def native_coroutine():
    return "古明地覺"

try:
    native_coroutine().__await__().__next__()
except StopIteration as e:
    print(e.value)  # 古明地覺

這里沒有創(chuàng)建事件循環(huán),而是直接驅(qū)動協(xié)程執(zhí)行。我們再演示一段代碼,看看讓生成器協(xié)程和原生協(xié)程混合使用會是什么效果。

import asyncio
import time
import types

async def some_task():
    """
    某個耗時較長的任務(wù)
    """
    await asyncio.sleep(3)
    return "task result"

async def native_coroutine():
    """
    原生協(xié)程
    """
    result = await some_task()
    return f"{result} from native coroutine"

@types.coroutine  # 或者使用 @asyncio.coroutine
def generator_coroutine():
    """
    生成器模擬的協(xié)程
    """
    result = yield from some_task()
    return f"{result} from generator coroutine"

async def main():
    start = time.time()
    result = await asyncio.gather(
        native_coroutine(), generator_coroutine()
    )
    end = time.time()
    print(result)
    print(f"耗時:{end - start}")

asyncio.run(main())
"""
['task result from native coroutine', 'task result from generator coroutine']
耗時:3.0016210079193115
"""

從效果上來看,兩種方式是等價的。yield from 會驅(qū)動協(xié)程對象執(zhí)行,當(dāng)協(xié)程執(zhí)行 return 的時候,會拋出一個 StopIteration 異常。然后 yield from 再將異常捕獲掉,并取出里面的返回值。

但使用裝飾器 + yield from 這種方式不夠優(yōu)雅,并且 yield from 即用于生成器,又用于協(xié)程,容易給人造成困惑。為此 Python 從 3.5 開始引入了原生協(xié)程,使用 async def  定義協(xié)程,使用 await 驅(qū)動協(xié)程執(zhí)行。

關(guān)于協(xié)程的更多細(xì)節(jié),后續(xù)在介紹協(xié)程的時候再說,總之我們現(xiàn)在應(yīng)該使用原生協(xié)程,至于 yield from 就讓它留在歷史的塵埃中吧,我們只需要知道整個演進過程即可。

小結(jié)

以上我們就從 Python 的角度梳理了一遍生成器相關(guān)的知識,下一篇文章我們將從源碼的角度來分析生成器的具體實現(xiàn)。

責(zé)任編輯:武曉燕 來源: 古明地覺的編程教室
相關(guān)推薦

2024-09-26 07:27:27

2025-06-04 03:21:00

RAGRetrievalGeneratio

2021-10-20 08:49:30

Vuexvue.js狀態(tài)管理模式

2023-08-01 08:27:15

Java I/ONIO

2019-01-24 09:46:38

PelicanPython生成器

2022-03-03 08:30:41

GeneratorES6函數(shù)

2023-10-16 08:16:31

Bean接口類型

2023-02-10 10:56:56

KubernetesLimitsRequests

2024-04-12 12:19:08

語言模型AI

2022-09-29 10:26:59

iOSScaffoldflutter

2022-03-24 08:51:48

Redis互聯(lián)網(wǎng)NoSQL

2022-05-05 16:47:24

Docker網(wǎng)絡(luò)空間容器

2025-05-22 06:23:48

2022-10-12 07:24:18

大文件哈希算法Hash

2019-01-29 09:18:00

開源代碼GitHub

2018-02-02 11:17:42

IaaSPaaSSaaS

2020-12-21 07:54:46

CountDownLa用法源碼

2019-11-06 17:30:57

cookiesessionWeb

2014-10-20 10:23:37

2020-05-15 16:37:13

PowerBI數(shù)據(jù)分析
點贊
收藏

51CTO技術(shù)棧公眾號

少妇av一区二区三区| 91福利精品视频| 狠狠色综合色区| 91午夜精品亚洲一区二区三区| 欧洲三级视频| 日韩精品一区二区在线观看| 久久综合九色综合88i| 麻豆国产在线播放| 国产一区二区三区四区在线观看| 久久久久久香蕉网| 99久久精品免费视频| 国产一区二区三区免费观看在线 | 日韩精品高清在线| 2025韩国理伦片在线观看| 欧洲中文在线| 国产精品免费视频网站| 国产日韩亚洲精品| 一区二区美女视频| 久久一区二区三区四区五区| 九九热精品在线| 人妻av无码一区二区三区| 日韩高清二区| 欧美婷婷六月丁香综合色| 91黄色在线看| 国产欧美黑人| 亚洲国产精品激情在线观看| 精品欧美日韩在线| 亚洲国产日韩在线观看| 久久99精品久久久久久久久久久久| 91av视频导航| 动漫精品一区一码二码三码四码| 欧美第十八页| 国产一区二区三区高清在线观看| 99久久免费看精品国产一区| 日韩欧美久久| 欧美一区日韩一区| 天天干天天操天天做| 日韩影片中文字幕| 欧美色播在线播放| 蜜臀av色欲a片无码精品一区| 老司机午夜在线| 国产精品欧美久久久久一区二区| 欧美激情一区二区三区在线视频| 农村少妇久久久久久久| 高清不卡在线观看av| 91色p视频在线| 一级淫片免费看| 蜜臀av一区二区| 国产精品影片在线观看| 中文字幕在线观看第二页| 视频一区二区三区在线| 国产va免费精品高清在线| 国产一级做a爱片久久毛片a| 一本综合久久| 日韩av日韩在线观看| 中文字幕69页| 日本美女一区二区三区| 国产精品成人av在线| 中文字幕在线天堂| 日韩成人免费电影| 国产日韩av在线| 国产又黄又粗又长| 国产精品性做久久久久久| 91福利视频导航| 精品国产无码一区二区三区| 国产凹凸在线观看一区二区| 动漫精品视频| 欧美偷拍视频| 欧美国产综合一区二区| 婷婷视频在线播放| 日本高清在线观看| 精品久久久一区| 亚洲性生活网站| 日韩深夜福利网站| 欧美v日韩v国产v| 内射中出日韩无国产剧情| 国产欧美日韩影院| 日韩视频欧美视频| 久久这里只有精品国产| 国产精品日韩欧美一区| 国产精品中文字幕在线| a天堂中文在线观看| 不卡视频免费播放| 日韩欧美视频一区二区| 二区在线播放| 午夜精品久久久久久久99樱桃| aⅴ在线免费观看| **精品中文字幕一区二区三区| 日韩精品一区二区三区蜜臀| 亚洲国产无码精品| 天天久久综合| **欧美日韩vr在线| 91影院在线播放| 成人av资源在线观看| 日韩福利在线| 丰满的护士2在线观看高清| 91成人看片片| 精品少妇人妻av一区二区三区| 欧美日韩国产在线观看网站 | 在线中文字幕播放| 欧美日韩久久一区二区| 午夜免费福利影院| 91视频一区| 欧美在线www| 99久久夜色精品国产亚洲| 久久久不卡网国产精品二区| 免费高清一区二区三区| 黄色成人在线观看网站| 亚洲黄色在线观看| www青青草原| 免费成人av资源网| 久久99精品久久久久久三级 | 日本中文字幕久久看| 国产片在线播放| 国产欧美日韩不卡免费| 免费看国产一级片| 久久99成人| 视频在线观看一区二区| av网站中文字幕| 国产成人午夜视频| 在线免费观看成人网| www.com.cn成人| 亚洲电影在线看| 2021亚洲天堂| 国产在线精品一区二区三区不卡 | 欧美大片免费播放器| 一区二区三区四区在线观看国产日韩| 国产ts一区二区| 午夜在线视频免费| 亚洲一二三区在线观看| 日韩av影视大全| 97国产精品| 国产精品中文字幕在线| 第九色区av在线| 色天使久久综合网天天| 青青草福利视频| 性8sex亚洲区入口| 国内精品视频免费| av成人福利| 欧美精品一区二区精品网| 视频这里只有精品| 国产精品18久久久| av久久久久久| 中文字幕一区日韩精品| 精品中文字幕在线| 国产农村妇女毛片精品| 亚洲免费在线观看视频| 免费高清视频在线观看| 影视一区二区| 97人人模人人爽人人喊38tv| 色噜噜狠狠狠综合欧洲色8| 日韩精品一区二区三区在线播放 | 日韩在线国产| 97人人做人人爽香蕉精品| 伊人伊人伊人久久| 中文字幕在线观看精品| 亚洲男同1069视频| 亚洲av无码成人精品区| 黄色在线成人| 国产综合 伊人色| 亚洲黄色网址| 亚洲一级免费视频| 一女二男一黄一片| 亚洲日本在线天堂| 99热超碰在线| 性高湖久久久久久久久| 日韩av图片| 电影中文字幕一区二区| 国内精品一区二区三区| 日本大片在线观看| 欧美日韩久久不卡| 激情综合网五月天| 久久影院午夜论| 五月激情婷婷在线| 尹人成人综合网| 欧美亚洲免费高清在线观看| 久久人体av| 欧美精品激情在线观看| 清纯唯美亚洲色图| 欧美男人的天堂一二区| 久久久久久久久久久网| 久久久久久久精| 成人免费黄色av| 久久久久国产一区二区| 裸体裸乳免费看| 西野翔中文久久精品字幕| 国产精品吴梦梦| av老司机在线观看| 色偷偷91综合久久噜噜| 免费观看黄色一级视频| 欧美丝袜自拍制服另类| 免费一级a毛片夜夜看| 久久久久免费观看| 欧美69精品久久久久久不卡| 久久婷婷麻豆| 日韩精品一区二区免费| 精品国产乱码久久久久久果冻传媒| 亚洲a一级视频| 日韩成人av电影| 欧美激情性做爰免费视频| 国产鲁鲁视频在线观看免费| 精品国产伦一区二区三区免费| 中文文字幕一区二区三三| 亚洲成av人**亚洲成av**| 成年人视频软件| www激情久久| 亚洲午夜久久久久久久久| 久久国产精品区| www.中文字幕在线| 综合天堂久久久久久久| 天天爽天天狠久久久| 国产一区丝袜| 99国产超薄肉色丝袜交足的后果| 国产成人毛片| 国产精品r级在线| 国产高清中文字幕在线| 欧美精品在线免费| 日本美女高清在线观看免费| 亚洲欧美制服第一页| 女人18毛片水真多18精品| 日韩欧美一二三| 亚洲一二区视频| 欧美在线一二三| 日韩三级一区二区| 精品magnet| 日本中文字幕网| 一区二区三区精品视频| 久草视频手机在线| 中文子幕无线码一区tr| 国产毛片欧美毛片久久久| 97se狠狠狠综合亚洲狠狠| 国产高潮视频在线观看| 国产91在线看| 性折磨bdsm欧美激情另类| 国产一区二区三区久久久| 一级黄色特级片| 另类成人小视频在线| 一区二区xxx| 日韩激情一二三区| 久久久国产欧美| 久久久久国产精品午夜一区| 91免费视频网站在线观看| 国产欧美91| 欧美黄色免费影院| 久久先锋资源| 在线免费视频a| 蜜臀91精品一区二区三区| 在线免费观看视频黄| 免费成人你懂的| 图片区乱熟图片区亚洲| 国产盗摄一区二区三区| 国产吃瓜黑料一区二区| 成人免费精品视频| 艳妇乳肉亭妇荡乳av| 久久夜色精品国产噜噜av| 亚洲自拍偷拍图| 欧美国产一区视频在线观看| 色偷偷www8888| 亚洲一区二区三区四区在线 | 欧美一级在线观看| 午夜精品久久久久久久99老熟妇| 欧美大片在线观看| 亚洲日本中文字幕在线| 亚洲视频在线观看视频| av在线电影网| 欧美精品在线网站| 激情视频网站在线播放色| 国产91色在线|免| 9999精品免费视频| 国产一区免费在线| 少妇精品久久久一区二区| 亚洲一区二区在线看| 欧美日韩视频一区二区三区| ww国产内射精品后入国产| 久久五月激情| 4438x全国最大成人| av电影在线观看一区| 99久久99久久精品免费| 一区二区三区四区中文字幕| 特一级黄色大片| 欧美人与禽zozo性伦| 黄色片一区二区三区| 亚洲天堂男人天堂| 国产美女福利在线| 日本精品久久中文字幕佐佐木| 亚洲老司机网| 玖玖玖精品中文字幕| 欧美韩日一区| 日日橹狠狠爱欧美超碰| 久久99精品久久久久久国产越南| 国产精品久久久久久亚洲av| 国产精品美女久久久久久| 久久免费视频精品| 欧美三级电影一区| 日本韩国在线观看| 色婷婷久久av| 亚洲精品88| 91久久精品国产91久久性色tv| 国产一区二区三区天码| www.射射射| 狠狠色丁香久久婷婷综合丁香| 老司机福利av| 一区二区三区不卡视频| 最新国产中文字幕| 日韩高清欧美高清| 日本无删减在线| 国产欧亚日韩视频| 九九热精品视频在线观看| 亚洲 自拍 另类小说综合图区| 国内精品伊人久久久久av影院 | 亚洲欧美日韩国产手机在线 | 视频一区视频二区中文字幕| 国产高潮失禁喷水爽到抽搐 | 成人国产精品久久| 欧美一区二区三区四区夜夜大片| 国内精品久久久久久久影视蜜臀 | 成人av中文字幕| 国产麻豆视频在线观看| 91搞黄在线观看| 欧美日韩视频精品二区| 97精品伊人久久久大香线蕉| 亚洲91网站| 中文字幕免费在线不卡| 日本不卡免费在线视频| 偷拍女澡堂一区二区三区| 亚洲国产精品人人做人人爽| 99久久精品国产一区二区成人| 日韩最新在线视频| 久久爱.com| 亚洲欧美久久久久一区二区三区| 亚洲男女自偷自拍| 亚洲国产精品成人综合久久久| 亚洲一区二区三区中文字幕在线| 国产女人18毛片18精品| 最好看的2019的中文字幕视频| 精品日韩视频| 日韩精品久久一区| 日本视频在线一区| 国产第一页精品| 欧美日韩一区二区三区四区| 成人jjav| 91精品久久久久久久久| 99精品视频在线| 婷婷中文字幕在线观看| 亚洲欧美色图小说| 99久久精品国产色欲| 欧美风情在线观看| 成人av综合网| 波多野结衣家庭教师在线播放| 91麻豆免费在线观看| 日韩熟女一区二区| 伊人精品在线观看| 国产精品久久久久久久久久齐齐| 亚洲视频sss| 韩国三级在线一区| 中文字幕av免费在线观看| 精品久久一区二区| 亚洲天堂电影| 视频一区二区三| 极品少妇xxxx偷拍精品少妇| 一区二区视频免费看| 精品日韩在线观看| www.日韩| 亚洲午夜精品久久久中文影院av| 激情五月婷婷综合| 国产亚洲欧美精品久久久久久| 亚洲第一网站免费视频| 日韩免费电影| 精品91一区二区三区| 菠萝蜜视频在线观看一区| 91在线视频在线观看| 色噜噜狠狠狠综合曰曰曰| 美国十次综合久久| 久久久999免费视频| 欧美激情一区二区三区不卡 | 久久奇米777| 在线中文字幕网站| 欧美激情国产精品| 九九久久成人| www.偷拍.com| 欧美视频不卡中文| 免费大片黄在线观看视频网站| 99久久99久久| 日韩国产一区二| 欧美黄色一级网站| 在线成人激情黄色| 中文无码日韩欧| 午夜国产一区二区三区| 亚洲午夜免费视频| eeuss影院www在线观看| 国产女主播一区二区| 美女视频黄免费的久久| 成人免费区一区二区三区| 久久精品视频一| 久操精品在线| 97精品人妻一区二区三区蜜桃| 欧美日韩一区二区三区在线 | 日本韩国精品在线| 欧美伦理免费在线| 色99中文字幕|