python協程--yield和yield from

字典爲動詞「to yield」給出了兩個釋義:產出和讓步。對於 Python 生成器中的 yield 來講,這兩個含義都成立。yield item 這行代碼會產出一個值,提供給 next(...) 的調用方;此外,還會做出讓步,暫停執行生成器,讓調用方繼續工做,直到須要使用另外一個值時再調用 next()。調用方會從生成器中拉取值。python

 

從句法上看,協程與生成器相似,都是定義體中包含 yield 關鍵字的函數。但是,在協程中,yield 一般出如今表達式的右邊(例如,datum = yield),能夠產出值,也能夠不產出----yield 關鍵字後面沒有表達式。協程可能會從調用方接收數據,調用方使用 .send(datum) 方法把數據提供給協程。多線程

 

 

一:生成器如何進化成協程函數

自python中加入yield關鍵字後,又通過了一系列的演化:post

yield 關鍵字能夠在表達式中使用(a = yield b);優化

生成器 API 中增長了.send(value) 方法(生成器的調用方可使用 .send(...) 方法發送數據,發送的數據會成爲生成器函數中 yield 表達式的值);ui

PEP 342 添加了 .throw(...) 和 .close() 方法(前者的做用是讓調用方拋出異常,在生成器中處理;後者的做用是終止生成器);spa

 

所以,生成器能夠做爲協程使用。協程是指一個過程,這個過程與調用方協做,產出由調用方提供的值。線程

 

協程最近的演進來自 Python 3.3實現的「PEP 380—Syntax for Delegating to a Subgenerator」(https://www.python.org/dev/peps/pep-0380/)。PEP 380 對生成器函數的句法作了兩處改動:code

生成器能夠返回一個值;之前若是在生成器中給 return 語句提供值,會拋出 SyntaxError 異常;orm

新引入了 yield from 句法,使用它能夠把複雜的生成器重構成小型的嵌套生成器,省去了以前把生成器的工做委託給子生成器所需的大量樣板代碼。

 

 

二:用做協程的生成器的基本行爲

協程能夠身處四個狀態中的一個。當前狀態可使用inspect.getgeneratorstate(...) 函數肯定,該函數會返回下述字符串中的一個。

GEN_CREATED:等待開始執行;

GEN_RUNNING:解釋器正在執行(只有在多線程應用中才能看到這個狀態);

GEN_SUSPENDED:在 yield 表達式處暫停;

GEN_CLOSED:執行結束;

 

         一個簡單的例子以下;

>>> def simple_coro2(a):
...     print('-> Started: a =', a)
...     b = yield a
...     print('-> Received: b =', b)
...     c = yield a + b
...     print('-> Received: c =', c)
...
>>> my_coro2 = simple_coro2(14)
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(my_coro2)
'GEN_CREATED'
>>> next(my_coro2)
-> Started: a = 14
14
>>> getgeneratorstate(my_coro2)
'GEN_SUSPENDED'
>>> my_coro2.send(28)
-> Received: b = 28
42
>>> my_coro2.send(99)
-> Received: c = 99
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> getgeneratorstate(my_coro2)
'GEN_CLOSED' 

最早調用 next(my_coro2) 函數這一步一般稱爲「預激」(prime)協程(即,讓協程向前執行到第一個 yield 表達式,準備好做爲活躍的協程使用)。

 

關鍵的一點是,協程在 yield 關鍵字所在的位置暫停執行。在賦值語句中,=右邊的代碼在賦值以前執行。所以,對於 b = yield a 這行代碼來講,等到客戶端代碼再激活協程時纔會設定 b 的值。

simple_coro2 協程的執行過程分爲 3 個階段,以下圖所示:

 

 

三:使用協程計算移動平均值

下面是一個計算移動平均值的協程:

def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average
        total += term
        count += 1
        average = total/count

>>> coro_avg = averager()
>>> next(coro_avg)   #調用 next 函數,預激協程
>>> coro_avg.send(10)
10.0
>>> coro_avg.send(30)
20.0
>>> coro_avg.send(5)
15.0

 

這個無限循環代表,只要調用方不斷把值發給這個協程,它就會一直接收值,而後生成結果。僅當調用方在協程上調用 .close() 方法,或者沒有對協程的引用而被垃圾回收程序回收時,這個協程纔會終止。

調用 next(coro_avg) 函數後,協程會向前執行到yield 表達式,產出 average 變量的初始值——None,所以不會出如今控制檯中。此時,協程在 yield 表達式處暫停,等到調用方發送值。coro_avg.send(10) 那一行發送一個值,激活協程,把發送的值賦給 term,並更新 total、count 和 average 三個變量的值,而後開始 while 循環的下一次迭代,產出 average 變量的值,等待下一次爲term 變量賦值。

 

 

四:預激協程的裝飾器

若是不預激,那麼協程沒什麼用。調用 my_coro.send(x) 以前,記住必定要調用next(my_coro)。爲了簡化協程的用法,有時會使用一個預激裝飾器。

下面就是一個預激裝飾器的例子(Python3):

from functools import wraps

def coroutine(func):
    @wraps(func)
    def primer(*args,**kwargs):
        gen = func(*args,**kwargs)
        next(gen)
        return gen
    return primer    

@coroutine
def averager2():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average
        total += term
        count += 1
        average = total/count

>>> coro_avg = averager()
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(coro_avg)
'GEN_SUSPENDED'
>>> coro_avg.send(10)
10.0
>>> coro_avg.send(30)
20.0
>>> coro_avg.send(5)
15.0

 

注意,使用 yield from 句法調用協程時,會自動預激。

 

 

五:終止協程和異常處理

協程中未處理的異常會向上冒泡,傳給 next 函數或 send 方法的調用方(即觸發協程的對象)。

>>> from coroaverager1 import averager
>>> coro_avg = averager()
>>> coro_avg.send(40)
40.0
>>> coro_avg.send(50)
45.0
>>> coro_avg.send('spam')
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +=: 'float' and 'str'
>>> coro_avg.send(60)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

 

因爲在協程內沒有處理異常,協程會終止。若是試圖從新激活協程,會拋出StopIteration 異常。

 

從 Python 2.5 開始,客戶代碼能夠在生成器對象上調用兩個方法:throw 和 close,顯式地把異常發給協程。

1:generator.throw(exc_type[, exc_value[, traceback]])

使生成器在暫停的 yield 表達式處拋出指定的異常。若是生成器處理了拋出的異常,代碼會向前執行到下一個 yield 表達式,而產出的值會成爲調用 generator.throw方法獲得的返回值。若是生成器沒有處理拋出的異常,異常會向上冒泡,傳到調用方的上下文中。

         2:generator.close()

使生成器在暫停的 yield 表達式處拋出 GeneratorExit 異常。若是生成器沒有處理這個異常,或者拋出了 StopIteration 異常(一般是指運行到結尾),調用方不會報錯。若是收到 GeneratorExit 異常,生成器必定不能產出值,不然解釋器會拋出RuntimeError 異常。生成器拋出的其餘異常會向上冒泡,傳給調用方。

 

         示例以下:

class DemoException(Exception):
    """爲此次演示定義的異常類型。"""
    
def demo_exc_handling():
    print('-> coroutine started')
    while True:
        try:
            x = yield
        except DemoException:
            print('*** DemoException handled. Continuing...')
        else:
            print('-> coroutine received: {!r}'.format(x))
    raise RuntimeError('This line should never run.')
    
>>> exc_coro = demo_exc_handling()
>>> next(exc_coro)
-> coroutine started
>>> exc_coro.send(11)
-> coroutine received: 11
>>> exc_coro.send(22)
-> coroutine received: 22

>>> exc_coro.throw(DemoException)
*** DemoException handled. Continuing...
>>> getgeneratorstate(exc_coro)
'GEN_SUSPENDED'

>>> exc_coro.close()
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(exc_coro)
'GEN_CLOSED'

 

 

 

六:讓協程返回值

在Python2中,生成器函數中的return不容許返回附帶返回值。在Python3中取消了這一限制,於是容許協程能夠返回值:

from collections import namedtuple
Result = namedtuple('Result', 'count average')

def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield
        if term is None:
            break
        total += term
        count += 1
        average = total/count
    return Result(count, average)
    
>>> coro_avg = averager()
>>> next(coro_avg)
>>> coro_avg.send(10)
>>> coro_avg.send(30)
>>> coro_avg.send(6.5)
>>> coro_avg.send(None)
Traceback (most recent call last):
...
StopIteration: Result(count=3, average=15.5)    

 

發送 None 會終止循環,致使協程結束,返回結果。一如既往,生成器對象會拋出StopIteration 異常。異常對象的 value 屬性保存着返回的值。

 

注意,return 表達式的值會偷偷傳給調用方,賦值給 StopIteration 異常的一個屬性。這樣作有點不合常理,可是能保留生成器對象的常規行爲——耗盡時拋出StopIteration 異常。若是須要接收返回值,能夠這樣:

>>> try:
...    coro_avg.send(None)
... except StopIteration as exc:
...    result = exc.value
...
>>> result
Result(count=3, average=15.5)

 

獲取協程的返回值要繞個圈子,可使用Python3.3引入的yield from獲取返回值。yield from 結構會在內部自動捕獲 StopIteration 異常。這種處理方式與 for 循環處理 StopIteration 異常的方式同樣。對 yield from 結構來講,解釋器不只會捕獲 StopIteration 異常,還會把value 屬性的值變成 yield from 表達式的值。

 

 

七:使用yield from

yield from 是 Python3.3 後新加的語言結構。在其餘語言中,相似的結構使用 await 關鍵字,這個名稱好多了,由於它傳達了相當重要的一點:在生成器 gen 中使用 yield from subgen() 時,subgen 會得到控制權,把產出的值傳給 gen 的調用方,即調用方能夠直接控制 subgen。與此同時,gen 會阻塞,等待 subgen 終止。

 

yield from 可用於簡化 for 循環中的 yield 表達式。例如:

>>> def gen():
... for c in 'AB':
...     yield c
... for i in range(1, 3):
...     yield i
...
>>> list(gen())
['A', 'B', 1, 2]

 

能夠改成

>>> def gen():
...     yield from 'AB'
...     yield from range(1, 3)
...
>>> list(gen())
['A', 'B', 1, 2]

 

yield from x 表達式對 x 對象所作的第一件事是,調用 iter(x),從中獲取迭代器。所以,x 能夠是任何可迭代的對象。

 

若是 yield from 結構惟一的做用是替代產出值的嵌套 for 循環,這個結構頗有可能不會添加到 Python 語言中。

yield from 的主要功能是打開雙向通道,把最外層的調用方與最內層的子生成器鏈接起來,這樣兩者能夠直接發送和產出值,還能夠直接傳入異常,而不用在位於中間的協程中添加大量處理異常的樣板代碼。有了這個結構,協程能夠經過之前不可能的方式委託職責。

 

PEP 380 使用了一些yield from使用的專門術語:

委派生成器:包含 yield from <iterable> 表達式的生成器函數;

子生成器:從 yield from 表達式中 <iterable> 部分獲取的生成器;

調用方:調用委派生成器的客戶端代碼;

        

下圖是這三者之間的交互關係:

 

委派生成器在 yield from 表達式處暫停時,調用方能夠直接把數據發給子生成器,子生成器再把產出的值發給調用方。子生成器返回以後,解釋器會拋出StopIteration 異常,並把返回值附加到異常對象上,此時委派生成器會恢復。

         下面是一個求平均身高和體重的示例代碼:

from collections import namedtuple

Result = namedtuple('Result', 'count average')

# 子生成器
def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        # main 函數發送數據到這裏 
        print("in averager, before yield")
        term = yield
        if term is None: # 終止條件
            break
        total += term
        count += 1
        average = total/count

    print("in averager, return result")
    return Result(count, average) # 返回的Result 會成爲grouper函數中yield from表達式的值


# 委派生成器
def grouper(results, key):
     # 這個循環每次都會新建一個averager 實例,每一個實例都是做爲協程使用的生成器對象
    while True:
        print("in grouper, before yield from averager, key is ", key)
        results[key] = yield from averager()
        print("in grouper, after yield from, key is ", key)


# 調用方
def main(data):
    results = {}
    for key, values in data.items():
        # group 是調用grouper函數獲得的生成器對象
        group = grouper(results, key)
        print("\ncreate group: ", group)
        next(group) #預激 group 協程。
        print("pre active group ok")
        for value in values:
            # 把各個value傳給grouper 傳入的值最終到達averager函數中;
            # grouper並不知道傳入的是什麼,同時grouper實例在yield from處暫停
            print("send to %r value %f now"%(group, value))
            group.send(value)
        # 把None傳入groupper,傳入的值最終到達averager函數中,致使當前實例終止。而後繼續建立下一個實例。
        # 若是沒有group.send(None),那麼averager子生成器永遠不會終止,委派生成器也永遠不會在此激活,也就不會爲result[key]賦值
        print("send to %r none"%group)
        group.send(None)
    print("report result: ")
    report(results)


# 輸出報告
def report(results):
    for key, result in sorted(results.items()):
        group, unit = key.split(';')
        print('{:2} {:5} averaging {:.2f}{}'.format(result.count, group, result.average, unit))


data = {
    'girls;kg':[40, 41, 42, 43, 44, 54],
    'girls;m': [1.5, 1.6, 1.8, 1.5, 1.45, 1.6],
    'boys;kg':[50, 51, 62, 53, 54, 54],
    'boys;m': [1.6, 1.8, 1.8, 1.7, 1.55, 1.6],
}

if __name__ == '__main__':
    main(data) 

grouper 發送的每一個值都會經由 yield from 處理,經過管道傳給 averager 實例。grouper 會在 yield from 表達式處暫停,等待 averager 實例處理客戶端發來的值。averager 實例運行完畢後,返回的值綁定到 results[key] 上。while 循環會不斷建立 averager 實例,處理更多的值。

外層 for 循環從新迭代時會新建一個 grouper 實例,而後綁定到 group 變量上。前一個 grouper 實例(以及它建立的還沒有終止的 averager 子生成器實例)被垃圾回收程序回收。

代碼結果以下:

 

create group:  <generator object grouper at 0x7f34ce8458e0>
in grouper, before yield from averager, key is  girls;kg
in averager, before yield
pre active group ok
send to <generator object grouper at 0x7f34ce8458e0> value 40.000000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce8458e0> value 41.000000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce8458e0> value 42.000000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce8458e0> value 43.000000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce8458e0> value 44.000000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce8458e0> value 54.000000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce8458e0> none
in averager, return result
in grouper, after yield from, key is  girls;kg
in grouper, before yield from averager, key is  girls;kg
in averager, before yield

create group:  <generator object grouper at 0x7f34ce845678>
in grouper, before yield from averager, key is  girls;m
in averager, before yield
pre active group ok
send to <generator object grouper at 0x7f34ce845678> value 1.500000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce845678> value 1.600000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce845678> value 1.800000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce845678> value 1.500000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce845678> value 1.450000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce845678> value 1.600000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce845678> none
in averager, return result
in grouper, after yield from, key is  girls;m
in grouper, before yield from averager, key is  girls;m
in averager, before yield

create group:  <generator object grouper at 0x7f34ce845620>
in grouper, before yield from averager, key is  boys;kg
in averager, before yield
pre active group ok
send to <generator object grouper at 0x7f34ce845620> value 50.000000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce845620> value 51.000000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce845620> value 62.000000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce845620> value 53.000000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce845620> value 54.000000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce845620> value 54.000000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce845620> none
in averager, return result
in grouper, after yield from, key is  boys;kg
in grouper, before yield from averager, key is  boys;kg
in averager, before yield

create group:  <generator object grouper at 0x7f34ce8458e0>
in grouper, before yield from averager, key is  boys;m
in averager, before yield
pre active group ok
send to <generator object grouper at 0x7f34ce8458e0> value 1.600000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce8458e0> value 1.800000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce8458e0> value 1.800000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce8458e0> value 1.700000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce8458e0> value 1.550000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce8458e0> value 1.600000 now
in averager, before yield
send to <generator object grouper at 0x7f34ce8458e0> none
in averager, return result
in grouper, after yield from, key is  boys;m
in grouper, before yield from averager, key is  boys;m
in averager, before yield
report result: 
 6 boys  averaging 54.00kg
 6 boys  averaging 1.68m
 6 girls averaging 44.00kg
 6 girls averaging 1.58m

 

這個試驗想代表的關鍵一點是,若是子生成器不終止,委派生成器會在yield from 表達式處永遠暫停。若是是這樣,程序不會向前執行,由於 yield from(與 yield 同樣)把控制權轉交給客戶代碼(即,委派生成器的調用方)了。

 

 

八:yield from的意義

把迭代器看成生成器使用,至關於把子生成器的定義體內聯在 yield from 表達式中。此外,子生成器能夠執行 return 語句,返回一個值,而返回的值會成爲 yield from 表達式的值。

PEP 380 在「Proposal」一節(https://www.python.org/dev/peps/pep-0380/#proposal)分六點說明了 yield from 的行爲。這裏幾乎原封不動地引述,不過把有歧義的「迭代器」一詞都換成了「子生成器」,還作了進一步說明。上面的示例闡明瞭下述四點:

子生成器產出的值都直接傳給委派生成器的調用方(即客戶端代碼);

使用 send() 方法發給委派生成器的值都直接傳給子生成器。若是發送的值是None,那麼會調用子生成器的 __next__() 方法。若是發送的值不是 None,那麼會調用子生成器的 send() 方法。若是子生成器拋出 StopIteration 異常,那麼委派生成器恢復運行。任何其餘異常都會向上冒泡,傳給委派生成器;

生成器退出時,生成器(或子生成器)中的 return expr 表達式會觸發StopIteration(expr) 異常拋出;

yield from 表達式的值是子生成器終止時傳給 StopIteration 異常的第一個參數。

 

yield from 的具體語義很難理解,尤爲是處理異常的那兩點。在PEP 380 中闡述了 yield from 的語義。還使用僞代碼(使用 Python 句法)演示了 yield from 的行爲。

若想研究那段僞代碼,最好將其簡化,只涵蓋 yield from 最基本且最多見的用法:yield from 出如今委派生成器中,客戶端代碼驅動着委派生成器,而委派生成器驅動着子生成器。爲了簡化涉及到的邏輯,假設客戶端沒有在委派生成器上調用throw(...) 或 close() 方法。並且假設子生成器不會拋出異常,而是一直運行到終止,讓解釋器拋出 StopIteration 異常。上面示例中的腳本就作了這些簡化邏輯的假設。

下面的僞代碼,等效於委派生成器中的 RESULT = yield from EXPR 語句(這裏針對的是最簡單的狀況:不支持 .throw(...) 和 .close() 方法,並且只處理 StopIteration 異常):

_i = iter(EXPR) 
try:
    _y = next(_i)
except StopIteration as _e:
    _r = _e.value
else:
    while 1:
        _s = yield _y
    try:
        _y = _i.send(_s)
    except StopIteration as _e:
        _r = _e.value
        break
RESULT = _r

 

 

可是,現實狀況要複雜一些,由於要處理客戶對 throw(...) 和 close() 方法的調用,而這兩個方法執行的操做必須傳入子生成器。此外,子生成器可能只是純粹的迭代器,不支持 throw(...) 和 close() 方法,所以 yield from 結構的邏輯必須處理這種狀況。若是子生成器實現了這兩個方法,而在子生成器內部,這兩個方法都會觸發異常拋出,這種狀況也必須由 yield from 機制處理。調用方可能會平白無故地讓子生成器本身拋出異常,實現 yield from 結構時也必須處理這種狀況。最後,爲了優化,若是調用方調用 next(...) 函數或 .send(None) 方法,都要轉交職責,在子生成器上調用next(...) 函數;僅當調用方發送的值不是 None 時,才使用子生成器的 .send(...) 方法。

         下面的僞代碼,是考慮了上述狀況以後,語句:RESULT = yield from EXPR的等效代碼:

_i = iter(EXPR)
try:
    _y = next(_i)
except StopIteration as _e:
    _r = _e.value
else:
    while 1:
        try:
            _s = yield _y
        except GeneratorExit as _e:
            try:
                _m = _i.close
            except AttributeError:
                pass
            else:
                _m()
            raise _e
        except BaseException as _e:
            _x = sys.exc_info()
            try:
                _m = _i.throw
            except AttributeError:
                raise _e
            else:
                try:
                    _y = _m(*_x)
                except StopIteration as _e:
                    _r = _e.value
                    break
        else:
            try:
                if _s is None:
                    _y = next(_i)
                else:
                    _y = _i.send(_s)
            except StopIteration as _e:
                _r = _e.value
                break
RESULT = _r

 

         上面的僞代碼中,會預激子生成器。這代表,用於自動預激的裝飾器與 yield from 結構不兼容。

 

 

《流暢的python》第16章

http://blog.gusibi.com/post/python-coroutine-yield-from/

相關文章
相關標籤/搜索