Python 3中的yield from語法詳解
前言
最近在搗鼓Autobahn,它有給出個例子是基於asyncio 的,想著說放到pypy3上跑跑看竟然就……失敗了。 pip install asyncio
直接報invalid syntax,粗看還以為2to3處理的時 候有問題——這不能怪我,好~多package都是用2寫了然後轉成3的——結果發 現asyncio本來就只支援3.3+的版本,才又回頭看程式碼,赫然發現一句 yield from
;yield
我知道,但是yield from
是神馬?
PEP-380
好吧這個標題是我google出來的,yield from
的前世今生都在 這個PEP裡面,總之大意是原本的yield
語句只能將CPU控制權 還給直接呼叫者,當你想要將一個generator或者coroutine裡帶有 yield語句的邏輯重構到另一個generator(原文是subgenerator) 裡的時候,會非常麻煩,因為外面的generator要負責為裡面的 generator做訊息傳遞;所以某人有個想法是讓python把訊息傳遞 封裝起來,使其對程式猿透明,於是就有了yield from
PEP-380規定了yield from
的語義,或者說巢狀的generator應該 有的行為模式。
假設A函式中有這樣一個語句
1 |
|
B()
返回的是一個可迭代(iterable)的物件b,那麼A()會返回一個 generator——照我們的命名規範,名字叫a——那麼:
- b迭代產生的每個值都直接傳遞給a的呼叫者。
- 所有通過send方法傳送到a的值都被直接傳遞給b. 如果傳送的 值是None,則呼叫b的
__next__()
方法,否則呼叫b的send 方法。如果對b的方法呼叫產生StopIteration異常,a會繼續 執行yield from
yield from
的時候丟擲異常。 - 如果有除GeneratorExit以外的異常被throw到a中的話,該異常 會被直接throw到b中。如果b的throw方法丟擲StopIteration, a會繼續執行;其他異常則會導致a也丟擲異常。
- 如果一個GeneratorExit異常被throw到a中,或者a的close 方法被呼叫了,並且b也有close方法的話,b的close方法也 會被呼叫。如果b的這個方法丟擲了異常,則會導致a也丟擲異常。 反之,如果b成功close掉了,a也會丟擲異常,但是是特定的 GeneratorExit異常。
- a中
yield from
表示式的求值結果是b迭代結束時丟擲的 StopIteration異常的第一個引數。 - b中的
return <expr>
語句實際上會丟擲StopIteration(<expr>)
異常,所以b中return的值會成為a中yield from
表示式的返回值。
為神馬會有這麼多要求?因為generator這種東西的行為在加入throw 方法之後變得非常複雜,特別是幾個generator在一起的情況,需要 類似程序管理的元語對其進行操作。上面的所有要求都是為了統一 generator原本就複雜的行為,自然簡單不下來啦。
我承認我一下沒看明白PEP的作者到底想說什麼,於是動手“重構” 一遍大概會有點幫助。
一個沒用的例子
說沒用是因為你大概不會真的想把程式寫成這樣,但是……反正能說明 問題就夠了。
設想有這樣一個generator函式:
1 2 3 4 5 6 7 8 9 10 11 |
|
這個函式生成的generator將從send方法接收到的值累加到區域性 變數total中,並且在收到BreakOut異常時停止迭代;至於另外 一個SwitchSign異常應該不難理解,這裡就不劇透了。
從程式碼上看,由inner()
函式得到的generator通過send接收用於 運算的資料,同時通過throw方法接受外部程式碼的控制以執行不同 的程式碼分支,目前為止都很清晰。
接下來因為需求有變動,我們需要在inner()
這段程式碼的前後分別加 入初始化和清理現場的程式碼。鑑於我認為“沒壞的程式碼就不要動”,我 決定讓inner()
維持現狀,然後再寫一個outer()
,把新增的程式碼放在 outer()
裡,並提供與inner()
一樣的操作介面。由於inner()
利用了 generator的若干特性,所以outer()
也必須做到這五件事情:
outer()
必須生成一個generator;- 在每一步的迭代中,
outer()
要幫助inner()
返回迭代值; - 在每一步的迭代中,
outer()
要幫助inner()
接收外部發送的資料; - 在每一步的迭代中,
outer()
要處理inner()
接收和丟擲所有異常; - 在
outer()
被close的時候,inner()
也要被正確地close掉。
根據上面的要求,在只有yield的世界裡,outer()
可能是長這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
WTF,這段程式碼比inner()
本身還要長,而且還沒處理close操作。
現在我們來試試外星科技:
1 2 3 4 |
|
除了完全符合上面的要求外,這四行程式碼打印出來的時候還能省點紙。
我們可以在outer1()
和outer2()
上分別測試 資料 以及 異常 的傳遞,不難發現這兩個generator的行為基本上是一致的。既然如此, 外星科技當然在大多數情況下是首選。
對generator和coroutine的疑問
從以前接觸到Python下的coroutine就覺得它怪怪的,我能看清它們的 行為模式,但是並不明白為什麼要使用這種模式,generator和 coroutine具有一樣的對外介面,是generator造就了coroutine呢,還 是coroutine造就了generator?最讓我百思不得其解的是,Python下 的coroutine將“訊息傳遞”和“排程”這兩種操作綁在一個yield 上——即便有了yield from
,這個狀況還是沒變過——我看不出這樣做 的必要性。如果一開始就從語法層面將這兩種語義分開,並且為 generator和coroutine分別設計一套介面,coroutine的概念大概也會 容易理解一些。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家學習或者使用python能帶來一定的幫助,如果有疑問大家可以留言交流。
2:https://blog.csdn.net/chenbin520/article/details/78111399?locationNum=7&fps=1 3:https://blog.csdn.net/Man_In_The_Night/article/details/80318263