首頁  >  文章  >  後端開發  >  Python 3中的yield from語法詳解

Python 3中的yield from語法詳解

高洛峰
高洛峰原創
2017-02-21 10:11:001347瀏覽

在python 3.3裡,generator新增了一個語法 yield from,這個yield from的作用是什麼?語法是什麼呢?以下透過這篇文章主要給大家詳細介紹了Python 3中yield from語法的相關資料,需要的朋友可以參考借鑒,下面來一起看看吧。

前言

最近在搗鼓Autobahn,它有給出個例子是基於asyncio 的,想著說放到pypy3上跑跑看竟然就…失敗了。 pip install asyncio直接報invalid syntax,粗看還以為2to3處理的時候有問題——這不能怪我,好~多package都是用2寫了然後轉成3的——結果發現asyncio本來就只支援3.3+的版本,才又回頭看程式碼,赫然發現一句yield fromyield我知道,但是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函數中有這樣一個語句

yield from B()

B()回傳的是一個可迭代(iterable )的物件b,那麼A()會回傳一個generator-照我們的命名規範,名字叫a-那麼:

  1. b迭代產生的每個值都直接傳遞給a的調用者。

  2. 所有透過send方法發送到a的值都直接傳遞給b. 如果發送的值是None,則呼叫b的__next__()方法,否則呼叫b的send 方法。如果對b的方法呼叫產生StopIteration異常,a會繼續執行yield from後面的語句,而其他異常則會傳播到a中,導致a在執行yield from的時候拋出例外.

  3. 如果有除GeneratorExit以外的異常被throw到a中的話,該異常 會被直接throw到b中。如果b的throw方法拋出StopIteration, a會繼續執行;其他異常則會導致a也拋出異常。

  4. 如果一個GeneratorExit異常被throw到a中,或者a的close 方法被呼叫了,並且b也有close方法的話,b的close方法也 會被呼叫。如果b的這個方法拋出了異常,則會導致a也拋出異常。 反之,如果b成功close掉了,a也會拋出異常,但是是特定的  GeneratorExit異常。

  5. a中yield from表達式的求值結果是b迭代結束時拋出的  StopIteration異常的第一個參數。

  6. b中的return 942bcdedd51e2f7d833cc5298f53fcca語句實際上會拋出StopIteration(942bcdedd51e2f7d833cc5298f53fcca) 異常,所以b中return的值會成為a中yield from表達式的回傳值。

為神馬會有這麼多要求?因為generator這種東西的行為在加入throw 方法之後變得非常複雜,特別是幾個generator在一起的情況,需要 類似進程管理的元語對其進行操作。上面的所有要求都是為了統一 generator原本就複雜的行為,自然簡單不下來啦。

我承認我一下沒看明白PEP的作者到底想說什麼,於是動手「重構」 一遍大概會有點幫助。

一個沒用的例子

說沒用是因為你大概不會真的想把程式寫成這樣,但是…反正能說明問題就夠了。

設想有這樣一個generator函數:

def inner():
 coef = 1
 total = 0
 while True:
 try:
  input_val = yield total
  total = total + coef * input_val
 except SwitchSign:
  coef = -(coef)
 except BreakOut:
  return total

這個函數產生的generator將從send方法接收到的值累加到局部變數total中,並且在收到BreakOut異常時停止迭代;至於另外一個SwitchSign異常應該不難理解,這裡就不劇透了。

從程式碼上看,由inner()函數得到的generator透過send接收用於運算的數據,同時透過throw方法接受外部程式碼的控制以執行不同的程式碼分支,目前為止都很清晰。

接下來因為需求有變動,我們需要在inner()這段程式碼的前後分別加 入初始化和清理現場的程式碼。鑑於我認為“沒壞的程式碼就不要動”,我決定讓inner()維持現狀,然後再寫一個outer() ,把添加的程式碼放在outer()裡,並提供與inner()一樣的操作介面。由於inner()利用了 generator的若干特性,所以outer()也必須做到這五件事:

  1. outer()必须生成一个generator;

  2. 在每一步的迭代中,outer()要帮助inner()返回迭代值;

  3. 在每一步的迭代中,outer()要帮助inner()接收外部发送的数据;

  4. 在每一步的迭代中,outer()要处理inner()接收和抛出所有异常;

  5. outer()被close的时候,inner()也要被正确地close掉。

根据上面的要求,在只有yield的世界里,outer()可能是长这样的:

def outer1():
 print("Before inner(), I do this.")
 i_gen = inner()
 input_val = None
 ret_val = i_gen.send(input_val)
 while True:
 try:
  input_val = yield ret_val
  ret_val = i_gen.send(input_val)
 except StopIteration:
  break
 except Exception as err:
  try:
  ret_val = i_gen.throw(err)
  except StopIteration:
  break
 print("After inner(), I do that.")

WTF,这段代码比inner()本身还要长,而且还没处理close操作。

现在我们来试试外星科技:

def outer2():
 print("Before inner(), I do this.")
 yield from inner()
 print("After inner(), I do that.")

除了完全符合上面的要求外,这四行代码打印出来的时候还能省点纸。

我们可以在outer1()outer2()上分别测试 数据 以及 异常 的传递,不难发现这两个generator的行为基本上是一致的。既然如此, 外星科技当然在大多数情况下是首选。

对generator和coroutine的疑问

从以前接触到Python下的coroutine就觉得它怪怪的,我能看清它们的 行为模式,但是并不明白为什么要使用这种模式,generator和 coroutine具有一样的对外接口,是generator造就了coroutine呢,还 是coroutine造就了generator?最让我百思不得其解的是,Python下 的coroutine将“消息传递”和“调度”这两种操作绑在一个yield 上——即便有了yield from,这个状况还是没变过——我看不出这样做 的必要性。如果一开始就从语法层面将这两种语义分开,并且为 generator和coroutine分别设计一套接口,coroutine的概念大概也会 容易理解一些。

更多Python 3中的yield from语法详解相关文章请关注PHP中文网!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn