Python3.3版本的PEP 380中添加了yield from
語法标锄,允許一個generator
生成器將其部分操作委派給另一個生成器。其產(chǎn)生的主要動力在于使生成器能夠很容易分為多個擁有send
和throw
方法的子生成器茁计,像一個大函數(shù)可以分為多個子函數(shù)一樣簡單。Python的生成器是協(xié)程coroutine
的一種形式谓松,但它的局限性在于只能向它的直接調(diào)用者yield值星压。這意味著那些包含yield的代碼不能想其他代碼那樣被分離出來放到一個單獨的函數(shù)中。這也正是yield from要解決的鬼譬。
雖然【yield from主要設(shè)計用來向子生成器委派操作任務(wù)】娜膘,但yield from可以向任意的迭代器委派操作;
對于簡單的迭代器优质,yield from iterable本質(zhì)上等于for item in iterable: yield item的縮寫版竣贪,如下所示:
>>> def g(x):
... yield from range(x, 0, -1)
... yield from range(x)
...
>>> list(g(5))
[5, 4, 3, 2, 1, 0, 1, 2, 3, 4]
然而,不同于普通的循環(huán)巩螃,yield from
允許子生成器直接從調(diào)用者接收其發(fā)送的信息或者拋出調(diào)用時遇到的異常演怎,并且返回給委派生產(chǎn)器一個值,如下所示:
>>> def accumulate(): # 子生成器避乏,將傳進的非None值累加爷耀,傳進的值若為None,則返回累加結(jié)果
... tally = 0 # 由于for循環(huán)被分配到子生成器去執(zhí)行拍皮,tally不斷的累加
... while 1:
... next = yield
... if next is None:
... return tally
... tally += next
...
>>> def gather_tallies(tallies): # 外部生成器歹叮,將累加操作任務(wù)委托給子生成器
... while 1:
... tally = yield from accumulate() # 這里for 會先執(zhí)行完 accumulate()后再from,把item 給yield跑杭,注意??程序執(zhí)行順序
... tallies.append(tally) # 由于子生成器累加后才把值append過來,所以最后結(jié)果是[6,10]
...
>>> tallies = []
>>> acc = gather_tallies(tallies)
>>> next(acc) # 使累加生成器準備好接收傳入值
>>> for i in range(4): # 這個for循環(huán)針對的是acc,按理說for循環(huán)作用于外部生成器咆耿,但實際上德谅,for 作用于子生成器,也就是上面說的 yield from 主要設(shè)計用來向子生成器委派操作任務(wù)的
... acc.send(i)
...
>>> acc.send(None) # 結(jié)束第一次累加
>>> for i in range(5):
... acc.send(i)
...
>>> acc.send(None) # 結(jié)束第二次累加
>>> tallies # 輸出最終結(jié)果
[6, 10]
總結(jié)如下:
- 迭代器(即可指子生成器)產(chǎn)生的值直接返還給調(diào)用者
- 任何使用
send()
方法發(fā)給委派生產(chǎn)器(即外部生產(chǎn)器)的值被直接傳遞給迭代器萨螺。如果send值是None窄做,則調(diào)用迭代器next()
方法;如果不為None屑迂,則調(diào)用迭代器的send()
方法浸策。如果對迭代器的調(diào)用產(chǎn)生StopIteration異常,委派生產(chǎn)器恢復(fù)繼續(xù)執(zhí)行yield from后面的語句惹盼;若迭代器產(chǎn)生其他任何異常庸汗,則都傳遞給委派生產(chǎn)器。 - 除了GeneratorExit 異常外的其他拋給委派生產(chǎn)器的異常手报,將會被傳遞到迭代器的throw()方法蚯舱。如果迭代器throw()調(diào)用產(chǎn)生了StopIteration異常,委派生產(chǎn)器恢復(fù)并繼續(xù)執(zhí)行掩蛤,其他異常則傳遞給委派生產(chǎn)器枉昏。
- 如果GeneratorExit異常被拋給委派生產(chǎn)器,或者委派生產(chǎn)器的close()方法被調(diào)用揍鸟,如果迭代器有close()的話也將被調(diào)用兄裂。如果close()調(diào)用產(chǎn)生異常,異常將傳遞給委派生產(chǎn)器阳藻。否則晰奖,委派生產(chǎn)器將拋出GeneratorExit 異常。
- 當(dāng)?shù)鹘Y(jié)束并拋出異常時腥泥,yield from表達式的值是其StopIteration 異常中的第一個參數(shù)匾南。
- 一個生成器中的return expr語句將會從生成器退出并拋出 StopIteration(expr)異常。
stackoverflow上的經(jīng)典解釋
將yield from視為提供了一個調(diào)用者和子生成器之間的透明的雙向通道蛔外。包括從子生成器獲取數(shù)據(jù)以及向子生成器傳送數(shù)據(jù)蛆楞。
1. 利用yield from從生成器讀取數(shù)據(jù)
def reader():
# 模擬從文件讀取數(shù)據(jù)的生成器
for i in range(4):
yield '<< %s' % i
def reader_wrapper(g):
# 循環(huán)迭代從reader產(chǎn)生的數(shù)據(jù)
for v in g:
yield v
wrap = reader_wrapper(reader())
for i in wrap:
print(i)
# 結(jié)果:
<< 0
<< 1
<< 2
<< 3
我們可以用yield from語句替代reader_wrapper(g)
函數(shù)中的循環(huán),如下:
def reader_wrapper(g):
yield from g
2.利用yield from語句向生成器(協(xié)程)傳送數(shù)據(jù)
首先創(chuàng)建一個生成器writer夹厌,接收傳送給它的數(shù)據(jù)豹爹,并寫進套接字,文件等矛纹;
def writer():
# 讀取send傳進的數(shù)據(jù)帅戒,并模擬寫進套接字或文件
while True:
w = yield # w接收send傳進的數(shù)據(jù)
print('>> ', w)
現(xiàn)在的問題是,包裝器函數(shù)如何傳送數(shù)據(jù)給writer函數(shù),使得傳遞給包裝器的數(shù)據(jù)都能夠顯示地傳遞給writer函數(shù)逻住?钟哥!
即我們期待得到如下結(jié)果:
def writer_wrapper(coro):
# TBD
pass
w = writer()
wrap = writer_wrapper(w)
wrap.send(None) # 生成器準備好接收數(shù)據(jù)
for i in range(4):
wrap.send(i)
# 期望結(jié)果:
>> 0
>> 1
>> 2
>> 3
我們可以用yield from
語句替代reader_wrapper(g)
函數(shù)中的循環(huán),如下:
def reader_wrapper(g):
yield from g
效果一樣瞎访,且代碼更簡潔有沒有腻贰。
2.利用yield from語句向生成器(協(xié)程)傳送數(shù)據(jù)
首先創(chuàng)建一個生成器writer,接收傳送給它的數(shù)據(jù)扒秸,并寫進套接字播演,文件等;
def writer():
# 讀取send傳進的數(shù)據(jù)伴奥,并模擬寫進套接字或文件
while True:
w = (yield) # w接收send傳進的數(shù)據(jù)
print('>> ', w)
現(xiàn)在的問題是写烤,包裝器函數(shù)如何傳送數(shù)據(jù)給writer函數(shù),使得傳遞給包裝器的數(shù)據(jù)都能夠顯示地傳遞給writer函數(shù)拾徙?洲炊!
即我們期待得到如下結(jié)果:
def writer_wrapper(coro):
# TBD
pass
w = writer()
wrap = writer_wrapper(w)
wrap.send(None) # 生成器準備好接收數(shù)據(jù)
for i in range(4):
wrap.send(i)
# 期望結(jié)果:
>> 0
>> 1
>> 2
>> 3
很顯然,包裝區(qū)需要接收數(shù)據(jù)并顯示傳遞給生成器尼啡,并且需要處理for循環(huán)耗盡是生成器產(chǎn)生的StopIteration異常暂衡,顯然包裝器只用for循環(huán)已經(jīng)不能滿足需求,滿足情況的一般版本如下:
def writer_wrapper(coro1):
coro1.send(None) # 生成器準備好接收數(shù)據(jù)
while True:
try:
x = (yield) # x接收send傳進的數(shù)據(jù)
coro1.send(x) # 然后將x在send給writer子生成器
except StopIteration: # 處理子生成器返回的異常
pass
包裝器也是個生成器崖瞭,上面所有復(fù)雜的寫法也可以用yield from
替換:
def writer_wrapper(coro2):
yield from coro2
一下子少了好多代碼狂巢,是不是見證了奇跡!
3. 利用yield from向生成器傳送數(shù)據(jù)–處理異常
更進一步书聚,如果我們的子生成器即writer需要處理異常該怎么辦唧领?假設(shè)writer需要處理SpamException
異常,遇到這個異常打印***雌续,代碼如下:
class SpamException(Exception):
pass
def writer():
while True:
try:
w = (yield)
except SpamException:
print('***')
else:
print('>> ', w)
如果使用上一個一般版本的包裝器writer_wrapper(coro1)
斩个,會有什么結(jié)果?試驗如下:
w = writer()
wrap = writer_wrapper(w)
wrap.send(None) # "prime" the coroutine
for i in [0, 1, 2, 'spam', 4]:
if i == 'spam':
wrap.throw(SpamException)
else:
wrap.send(i)
# 期望結(jié)果:
>> 0
>> 1
>> 2
***
>> 4
# 實際結(jié)果:
>> 0
>> 1
>> 2
Traceback (most recent call last):
File ... in <module>
wrap.throw(SpamException)
File ... in writer_wrapper
x = (yield)
__main__.SpamException
可以看出西雀,這行不通,因為x = (yield)
語句僅能夠引發(fā)異常歉摧,然后停止運行艇肴。我們可以手工在包裝器writer_wrapper(coro1)
中添加異常處理,并傳遞或者拋出異常給子生成器writer叁温,代碼如下:
def writer_wrapper(coro1):
# 手工處理異常被拋給子生成器
coro1.send(None) # 生成器準備好接收數(shù)據(jù)
while True:
try:
try:
x = (yield)
except Exception as e: # 捕獲異常
coro1.throw(e)
else:
coro1.send(x)
except StopIteration:
pass
同樣的再悼,這一堆復(fù)雜的代碼,也可以用yield from
語句替換膝但,并且功能完全一樣3寰拧!!
def writer_wrapper(coro):
yield from coro
重要的代碼貼三遍莺奸!三遍丑孩!三遍!
看到這里灭贷,大概能理解yield from
顯示處理傳值給子生成器以及拋出異常給子生成器的意思了吧温学。
當(dāng)然yield from
不僅有這兩個處理情況,還有之前我們提到的:外部生成器關(guān)閉甚疟,子生成器也會關(guān)閉仗岖;子生成器返回一個值得情況(上文第二個代碼例子),等等览妖。
總之轧拄,這是一個魔法語句,它也是協(xié)程的重要組成部分讽膏,至于協(xié)程檩电,還需要繼續(xù)學(xué)習(xí)。
協(xié)程最簡單易懂小例子(補)
Python中的協(xié)程和生成器很相似但又稍有不同是嗜。主要區(qū)別在于:
生成器是數(shù)據(jù)的生產(chǎn)者
協(xié)程則是數(shù)據(jù)的消費者
Python實現(xiàn)的grep就是個很好的例子:
def grep(pattern):
print("Searching for", pattern)
while True:
line = (yield) # 接受消息,消息中有pattern就print(line)
if pattern in line:
print(line)
等等挺尾!yield
返回了什么鹅搪?啊哈,我們已經(jīng)把它變成了一個協(xié)程遭铺。它將不再包含任何初始值丽柿,相反要從外部傳值給它。我們可以通過
send()
方法向它傳值魂挂。這有個例子:
search = grep('coroutine')
next(search)
#output: Searching for coroutine
search.send("I love you")
search.send("Don't you love me?")
search.send("I love coroutine instead!")
#output: I love coroutine instead!
發(fā)送的值會被yield
接收甫题。我們?yōu)槭裁匆\行next()
方法呢?這樣做正是為了啟動一個協(xié)程涂召。就像協(xié)程中包含的生成器并不是立刻執(zhí)行坠非,而是通過next()
方法來響應(yīng)send()
方法。因此果正,你必須通過next()
方法來執(zhí)行yield
表達式炎码。
我們可以通過調(diào)用close()
方法來關(guān)閉一個協(xié)程。像這樣:
search = grep('coroutine')
search.close()