摘要:于此同時,會阻塞,等待終止。子生成器返回之后,解釋器會拋出異常,并把返回值附加到異常對象上,只是委派生成器恢復。實例運行完畢后,返回的值綁定到上。這一部分處理調用方通過方法傳入的異常。之外的異常會向上冒泡。
上一篇python協程1:yield的使用介紹了:
生成器作為協程使用時的行為和狀態
使用裝飾器預激協程
調用方如何使用生成器對象的 .throw(...) 和 .close() 方法控制協程
這一篇將介紹:
協程終止時如何返回值
yield新句法的用途和語義
同時會用幾個協程的示例展示協程用法。
讓協程返回值先看一個例子:
這段代碼會返回最終均值的結果,每次激活協程時不會產出移動平均值,而是最后一次返回。
#! -*- coding: utf-8 -*- 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 # 返回一個namedtuple,包含count和average兩個字段。在python3.3前,如果生成器返回值,會報錯 return Result(count, average)
我們調用這段代碼,結果如下
>>> coro_avg = averager() >>> next(coro_avg) >>> coro_avg.send(20) # 并沒有返回值 >>> coro_avg.send(30) >>> coro_avg.send(40) >>> coro_avg.send(None) # 發送None終止循環,導致協程結束。生成器對象會拋出StopIteration異常。異常對象的value屬性保存著返回值。 Traceback (most recent call last): ... StopIteration: Result(count=3, average=30)
return 表達式的值會傳給調用方,賦值給StopIteration 異常的一個屬性。這樣做雖然看著別扭,但為了保留生成器對象耗盡時拋出StopIteration異常的行為,也可以理解。
如果我們想獲取協程的返回值,可以這么操作:
>>> coro_avg = averager() >>> next(coro_avg) >>> coro_avg.send(20) # 并沒有返回值 >>> coro_avg.send(30) >>> coro_avg.send(40) >>> try: ... coro_avg.send(None) ... except StopIteration as exc: ... result = exc.value ... >>> result Result(count=3, average=30)
看到這我們會說,這是什么鬼,為什么獲取返回值要繞這么一大圈,就沒有簡單的方法嗎?
有的,那就是 yield from
yield from 結果會在內部自動捕獲StopIteration 異常。這種處理方式與 for 循環處理StopIteration異常的方式一樣。
對于yield from 結構來說,解釋器不僅會捕獲StopIteration異常,還會把value屬性的值變成yield from 表達式的值。
在函數外部不能使用yield from(yield也不行)。
既然我們提到了 yield from 那yield from 是什么呢?
yield fromyield 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"]
下面來看一個復雜點的例子:(來自Python cookbook 3 ,github源碼地址 https://github.com/dabeaz/python-cookbook/blob/master/src/4/how_to_flatten_a_nested_sequence/example.py)
# Example of flattening a nested sequence using subgenerators from collections import Iterable def flatten(items, ignore_types=(str, bytes)): for x in items: if isinstance(x, Iterable) and not isinstance(x, ignore_types): yield from flatten(x) # 這里遞歸調用,如果x是可迭代對象,繼續分解 else: yield x items = [1, 2, [3, 4, [5, 6], 7], 8] # Produces 1 2 3 4 5 6 7 8 for x in flatten(items): print(x) items = ["Dave", "Paula", ["Thomas", "Lewis"]] for x in flatten(items): print(x)
yield from x 表達式對x對象做的第一件事是,調用 iter(x),獲取迭代器。所以要求x是可迭代對象。
PEP380 的標題是 ”syntax for delegating to subgenerator“(把指責委托給子生成器的句法)。由此我們可以知道,yield from是可以實現嵌套生成器的使用。
yield from 的主要功能是打開雙向通道,把最外層的調用方與最內層的子生成器連接起來,使兩者可以直接發送和產出值,還可以直接傳入異常,而不用在中間的協程添加異常處理的代碼。
yield from 包含幾個概念:
委派生成器
包含yield from
子生成器
從yield from
調用方
調用委派生成器的客戶端(調用方)代碼
這個示意圖是 對yield from 的調用過程
委派生成器在 yield from 表達式處暫停時,調用方可以直接把數據發給字生成器,子生成器再把產出的值發送給調用方。子生成器返回之后,解釋器會拋出StopIteration異常,并把返回值附加到異常對象上,只是委派生成器恢復。
這個圖來自于Paul
Sokolovsky 的 How Python 3.3 "yield from" construct works
下邊這個例子是對yield from 的一個應用:
#! -*- coding: utf-8 -*- from collections import namedtuple Result = namedtuple("Result", "count average") # 子生成器 # 這個例子和上邊示例中的 averager 協程一樣,只不過這里是作為字生成器使用 def averager(): total = 0.0 count = 0 average = None while True: # main 函數發送數據到這里 term = yield if term is None: # 終止條件 break total += term count += 1 average = total/count return Result(count, average) # 返回的Result 會成為grouper函數中yield from表達式的值 # 委派生成器 def grouper(results, key): # 這個循環每次都會新建一個averager 實例,每個實例都是作為協程使用的生成器對象 while True: # grouper 發送的每個值都會經由yield from 處理,通過管道傳給averager 實例。grouper會在yield from表達式處暫停,等待averager實例處理客戶端發來的值。averager實例運行完畢后,返回的值綁定到results[key] 上。while 循環會不斷創建averager實例,處理更多的值。 results[key] = yield from averager() # 調用方 def main(data): results = {} for key, values in data.items(): # group 是調用grouper函數得到的生成器對象,傳給grouper 函數的第一個參數是results,用于收集結果;第二個是某個鍵 group = grouper(results, key) next(group) for value in values: # 把各個value傳給grouper 傳入的值最終到達averager函數中; # grouper并不知道傳入的是什么,同時grouper實例在yield from處暫停 group.send(value) # 把None傳入groupper,傳入的值最終到達averager函數中,導致當前實例終止。然后繼續創建下一個實例。 # 如果沒有group.send(None),那么averager子生成器永遠不會終止,委派生成器也永遠不會在此激活,也就不會為result[key]賦值 group.send(None) 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)
這段代碼從一個字典中讀取男生和女生的身高和體重。然后把數據傳給之前定義的 averager 協程,最后生成一個報告。
執行結果為
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 from調用另一個生成器。最終以一個只是用yield表達式的生成器(或者任意可迭代對象)結束。
yield from 的意義PEP380 分6點說明了yield from 的行為。
子生成器產出的值都直接傳給委派生成器的調用方(客戶端代碼)
使用send() 方法發給委派生成器的值都直接傳給子生成器。如果發送的值是None,那么會調用子生成器的 __next__()方法。如果發送的值不是None,那么會調用子生成器的send()方法。如果調用的方法拋出StopIteration異常,那么委派生成器恢復運行。任何其他異常都會向上冒泡,傳給委派生成器。
生成器退出時,生成器(或子生成器)中的return expr 表達式會觸發 StopIteration(expr) 異常拋出。
yield from表達式的值是子生成器終止時傳給StopIteration異常的第一個參數。
傳入委派生成器的異常,除了 GeneratorExit 之外都傳給子生成器的throw()方法。如果調用throw()方法時拋出 StopIteration 異常,委派生成器恢復運行。StopIteration之外的異常會向上冒泡。傳給委派生成器。
如果把 GeneratorExit 異常傳入委派生成器,或者在委派生成器上調用close() 方法,那么在子生成器上調用close() 方法,如果他有的話。如果調用close() 方法導致異常拋出,那么異常會向上冒泡,傳給委派生成器;否則,委派生成器拋出 GeneratorExit 異常。
yield from的具體語義很難理解,不過我們可以看下Greg Ewing 的偽代碼,通過偽代碼分析一下:
RESULT = yield from EXPR # is semantically equivalent to # EXPR 可以是任何可迭代對象,因為獲取迭代器_i 使用的是iter()函數。 _i = iter(EXPR) try: _y = next(_i) # 2 預激字生成器,結果保存在_y 中,作為第一個產出的值 except StopIteration as _e: # 3 如果調用的方法拋出StopIteration異常,獲取異常對象的value屬性,賦值給_r _r = _e.value else: while 1: # 4 運行這個循環時,委派生成器會阻塞,只能作為調用方和子生成器直接的通道 try: _s = yield _y # 5 產出子生成器當前產出的元素;等待調用方發送_s中保存的值。 except GeneratorExit as _e: # 6 這一部分是用于關閉委派生成器和子生成器,因為子生成器可以是任意可迭代對象,所以可能沒有close() 方法。 try: _m = _i.close except AttributeError: pass else: _m() # 如果調用close() 方法導致異常拋出,那么異常會向上冒泡,傳給委派生成器;否則,委派生成器拋出 GeneratorExit 異常。 raise _e except BaseException as _e: # 7 這一部分處理調用方通過.throw() 方法傳入的異常。如果子生成器是迭代器,沒有throw()方法,這種情況會導致委派生成器拋出異常 _x = sys.exc_info() try: # 傳入委派生成器的異常,除了 GeneratorExit 之外都傳給子生成器的throw()方法。 _m = _i.throw except AttributeError: # 子生成器一迭代器,沒有throw()方法, 調用throw()方法時拋出AttributeError異常傳給委派生成器 raise _e else: # 8 try: _y = _m(*_x) except StopIteration as _e: # 如果調用throw()方法時拋出 StopIteration 異常,委派生成器恢復運行。 # StopIteration之外的異常會向上冒泡。傳給委派生成器。 _r = _e.value break else: # 9 如果產出值時沒有異常 try: # 10 嘗試讓子生成器向前執行 if _s is None: # 11. 如果發送的值是None,那么會調用子生成器的 __next__()方法。 _y = next(_i) else: # 11. 如果發送的值不是None,那么會調用子生成器的send()方法。 _y = _i.send(_s) except StopIteration as _e: # 12 # 2. 如果調用的方法拋出StopIteration異常,獲取異常對象的value屬性,賦值給_r, 退出循環,委派生成器恢復運行。任何其他異常都會向上冒泡,傳給委派生成器。 _r = _e.value break RESULT = _r #13 返回的結果是 _r 即整個yield from表達式的值
上段代碼變量說明:
_i 迭代器(子生成器)
_y 產出的值 (子生成器產出的值)
_r 結果 (最終的結果 即整個yield from表達式的值)
_s 發送的值 (調用方發給委派生成器的值,這個只會傳給子生成器)
_e 異常 (異常對象)
我們可以看到在代碼的第一個 try 部分 使用 _y = next(_i) 預激了子生成器。這可以看出,上一篇我們使用的用于自動預激的裝飾器與yield from 語句不兼容。
除了這段偽代碼之外,PEP380 還有個說明:
In a generator, the statement return value is semantically equivalent to raise StopIteration(value) except that, as currently, the exception cannot be caught by except clauses within the returning generator.
這也就是為什么 yield from 可以使用return 來返回值而 yield 只能使用 try ... except StopIteration ... 來捕獲異常的value 值。
>>> try: ... coro_avg.send(None) ... except StopIteration as exc: ... result = exc.value ... >>> result
到這里,我們已經了解了 yield from 的具體細節。下一篇,會分析一個使用協程的經典案例: 仿真編程。這個案例說明了如何使用協程在單線程中管理并發活動。
參考文檔流暢的python 第16章(這是讀書筆記,這是讀書筆記)
PEP 380-- Syntax for Delegating to a Subgenerator
How Python 3.3 "yield from" construct works
最后,感謝女朋友支持。
>歡迎關注 | >請我喝芬達 |
---|---|
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/44437.html
摘要:徘徊和行程所用的時間使用指數分布生成,我們將時間設為分鐘數,以便顯示清楚。迭代表示各輛出租車的進程在各輛出租車上調用函數,預激協程。 前兩篇我們已經介紹了python 協程的使用和yield from 的原理,這一篇,我們用一個例子來揭示如何使用協程在單線程中管理并發活動。。 什么是離散事件仿真 Wiki上的定義是: 離散事件仿真將系統隨時間的變化抽象成一系列的離散時間點上的事件,通過...
摘要:協程定義協程的底層架構是在中定義,并在實現的。為了簡化,我們會使用裝飾器預激協程。執行上述代碼結果如下出錯的原因是發送給協程的值不能加到變量上。示例使用和方法控制協程。 最近找到一本python好書《流暢的python》,是到現在為止看到的對python高級特性講述最詳細的一本。看了協程一章,做個讀書筆記,加深印象。 協程定義 協程的底層架構是在pep342 中定義,并在python2...
摘要:主程序通過喚起子程序并傳入數據,子程序處理完后,用將自己掛起,并返回主程序,如此交替進行。通過輪詢或是等事件框架,捕獲返回的事件。從消息隊列中取出記錄,恢復協程函數。然而事實上只有直接操縱的協程函數才有可能接觸到這個對象。 首發于 我的博客 轉載請注明出處 寫在前面 本文默認讀者對 Python 生成器 有一定的了解,不了解者請移步至生成器 - 廖雪峰的官方網站。 本文基于 Pyth...
摘要:協程的基本行為協程包含四種狀態等待開始執行。協程中重要的兩個方法調用方把數據提供給協程。注意使用調用協程時會自動預激,因此與裝飾器不兼容標準庫中的裝飾器不會預激協程,因此能兼容句法。因此,終止協程的本質在于向協程發送其無法處理的異常。 導語:本文章記錄了本人在學習Python基礎之控制流程篇的重點知識及個人心得,打算入門Python的朋友們可以來一起學習并交流。 本文重點: 1、掌握協...
摘要:并發用于制定方案,用來解決可能但未必并行的問題。在協程中使用需要注意兩點使用鏈接的多個協程最終必須由不是協程的調用方驅動,調用方顯式或隱式在最外層委派生成器上調用函數或方法。對象可以取消取消后會在協程當前暫停的處拋出異常。 導語:本文章記錄了本人在學習Python基礎之控制流程篇的重點知識及個人心得,打算入門Python的朋友們可以來一起學習并交流。 本文重點: 1、了解asyncio...
閱讀 2991·2021-10-19 11:46
閱讀 984·2021-08-03 14:03
閱讀 2941·2021-06-11 18:08
閱讀 2911·2019-08-29 13:52
閱讀 2758·2019-08-29 12:49
閱讀 487·2019-08-26 13:56
閱讀 929·2019-08-26 13:41
閱讀 851·2019-08-26 13:35