PEP 380 – 委派子生成器的語法
- 作者:
- Gregory Ewing <greg.ewing at canterbury.ac.nz>
- 狀態:
- 最終版
- 型別:
- 標準跟蹤
- 建立日期:
- 2009年2月13日
- Python 版本:
- 3.3
- 釋出歷史:
- 決議:
- Python-Dev 訊息
摘要
提出了一種語法,用於生成器將其部分操作委派給另一個生成器。這允許將包含“yield”的程式碼段分解並放置在另一個生成器中。此外,子生成器可以返回值,並且該值可供委派生成器使用。
新語法還為當一個生成器重新生成另一個生成器產生的值時提供了一些最佳化機會。
PEP 接受
Guido 於2011年6月26日正式接受了該PEP。
動機
Python 生成器是一種協程形式,但它有一個限制,即它只能向其直接呼叫者讓出。這意味著包含 yield 的程式碼段不能像其他程式碼一樣被分解並放入單獨的函式中。執行這種分解會導致被呼叫的函式本身成為一個生成器,並且有必要顯式迭代這個第二個生成器並重新讓出它產生的任何值。
如果只關心值的讓出,這可以透過使用以下迴圈輕鬆完成:
for v in g:
yield v
然而,如果子生成器要在呼叫 send()、throw() 和 close() 的情況下與呼叫者正確互動,事情會變得困難得多。正如後面將看到的,必要的程式碼非常複雜,並且很難正確處理所有特殊情況。
將提出一種新的語法來解決這個問題。在最簡單的用例中,它將等同於上面的 for 迴圈,但它也將處理完整的生成器行為,並允許以簡單直接的方式重構生成器程式碼。
提案
在生成器的主體中將允許以下新的表示式語法:
yield from <expr>
其中 <expr> 是一個求值為可迭代物件的表示式,從中提取一個迭代器。該迭代器將執行直到耗盡,在此期間它將值直接讓出或接收到包含 yield from 表示式的生成器的呼叫者(“委託生成器”)處。
此外,當迭代器是另一個生成器時,子生成器可以執行帶值的 return 語句,並且該值成為 yield from 表示式的值。
可以按照生成器協議描述 yield from 表示式的完整語義,如下所示:
- 迭代器產生的所有值都會直接傳遞給呼叫者。
- 任何透過
send()傳送給委託生成器的值都會直接傳遞給迭代器。如果傳送的值是 None,則呼叫迭代器的__next__()方法。如果傳送的值不是 None,則呼叫迭代器的send()方法。如果呼叫引發 StopIteration,則恢復委託生成器。任何其他異常都會傳播到委託生成器。 - 除 GeneratorExit 之外的拋入委託生成器的異常,會傳遞給迭代器的
throw()方法。如果呼叫引發 StopIteration,則恢復委託生成器。任何其他異常都會傳播到委託生成器。 - 如果將 GeneratorExit 異常拋入委託生成器,或者呼叫委託生成器的
close()方法,則如果迭代器有close()方法,則會呼叫該方法。如果此呼叫導致異常,則會傳播到委託生成器。否則,在委託生成器中引發 GeneratorExit。 yield from表示式的值是迭代器終止時引發的StopIteration異常的第一個引數。- 生成器中的
return expr會在生成器退出時引發StopIteration(expr)。
對 StopIteration 的增強
為了方便,StopIteration 異常將獲得一個 value 屬性,該屬性儲存其第一個引數,如果沒有引數則為 None。
正式語義
本節使用 Python 3 語法。
- 語句
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
- 在生成器中,語句
return value
語義上等同於
raise StopIteration(value)
除了,和當前一樣,異常不能被返回生成器中的
except子句捕獲。 - StopIteration 異常的行為就好像它是這樣定義的:
class StopIteration(Exception): def __init__(self, *args): if len(args) > 0: self.value = args[0] else: self.value = None Exception.__init__(self, *args)
基本原理
重構原則
上述語義大部分背後的原理源於對生成器程式碼進行重構的渴望。應該可以將包含一個或多個 yield 表示式的程式碼段,移動到一個單獨的函式中(使用處理周圍作用域中變數引用的常用技術等),並使用 yield from 表示式呼叫新函式。
在所有情況下,包括對 __next__()、send()、throw() 和 close() 的呼叫,生成的複合生成器的行為應在合理可行範圍內與原始未分解的生成器相同。
對於除生成器以外的子迭代器,語義被選擇為生成器情況的合理泛化。
擬議的語義在重構方面存在以下限制
- 一個捕獲 GeneratorExit 但隨後不重新引發它的程式碼塊,無法在保持完全相同行為的同時被分解。
- 如果將 StopIteration 異常拋入委派生成器,則分解後的程式碼可能與未分解的程式碼行為不一致。
鑑於這些用例很少甚至不存在,不認為為了支援它們而增加額外的複雜性是值得的。
終結化
關於是否在委派生成器暫停在 yield from 處時,透過呼叫其 close() 方法來顯式終結委派生成器也應該終結子迭代器,存在一些爭論。反對這樣做的論點是,如果對子迭代器的引用存在於其他地方,這將導致子迭代器過早終結。
考慮到非引用計數 Python 實現,我們決定應該執行這種顯式終結,以便在所有 Python 實現中,顯式關閉一個分解的生成器與關閉一個未分解的生成器具有相同的效果。
假設在大多數用例中,子迭代器不會被共享。共享子迭代器的罕見情況可以透過一個包裝器來解決,該包裝器會阻塞 throw() 和 close() 呼叫,或者透過使用 yield from 以外的方式來呼叫子迭代器。
作為執行緒的生成器
生成器能夠返回值的一個動機是,將生成器用於實現輕量級執行緒。當以這種方式使用生成器時,希望將輕量級執行緒執行的計算分散到許多函式中是很合理的。人們希望能夠像呼叫普通函式一樣呼叫子生成器,向其傳遞引數並接收返回值。
使用擬議的語法,像這樣的語句:
y = f(x)
其中 f 是一個普通函式,可以轉換為一個委託呼叫:
y = yield from g(x)
其中 g 是一個生成器。可以透過將 g 視為一個可以使用 yield 語句暫停的普通函式來推斷所生成程式碼的行為。
當以這種方式將生成器用作執行緒時,通常對透過 yield 傳入或傳出的值不感興趣。但是,也有這種用例,其中執行緒被視為專案的生產者或消費者。yield from 表示式允許將執行緒的邏輯分散到任意數量的函式中,專案的生產或消費可以在任何子函式中發生,並且這些專案會自動路由到其最終來源或目的地,或從中路由。
關於 throw() 和 close(),可以合理地預期,如果從外部將異常拋入執行緒,它應該首先線上程暫停的最內層生成器中引發,並從那裡向外傳播;如果從外部透過呼叫 close() 終止執行緒,則活動生成器鏈應從最內層向外完成。
語法
所提議的特定語法被選中,因為它暗示了其含義,同時沒有引入任何新關鍵字,並且明顯地與普通的 yield 不同。
最佳化
當存在長鏈生成器時,使用專用語法可以提供最佳化可能性。例如,當遞迴遍歷樹結構時,可能會出現這種鏈。在鏈中向下和向上傳遞 __next__() 呼叫和 yielded 值的開銷可能導致 O(n) 操作在最壞情況下變成 O(n**2)。
一種可能的策略是為生成器物件新增一個槽,以儲存正在委託的生成器。當對生成器進行 __next__() 或 send() 呼叫時,首先檢查該槽,如果它不為空,則恢復它引用的生成器。如果它引發 StopIteration,則清除該槽並恢復主生成器。
這將把委託開銷減少到一系列不涉及 Python 程式碼執行的 C 函式呼叫。一種可能的增強是遍歷整個生成器鏈並直接恢復末尾的那個,儘管那樣處理 StopIteration 會更復雜。
使用 StopIteration 返回值
有多種方式可以將生成器的返回值傳回。一些替代方案包括將其作為生成器-迭代器物件的屬性儲存,或者將其作為對子生成器的 close() 呼叫的值返回。然而,提議的機制具有幾個吸引人的原因:
- 使用 StopIteration 異常的泛化使得其他型別的迭代器能夠輕鬆參與協議,而無需增加額外的屬性或 close() 方法。
- 它簡化了實現,因為子生成器返回值可用的時間點與異常引發的時間點相同。延遲到任何更晚的時間都需要在某個地方儲存返回值。
被拒絕的想法
一些想法被討論但被否決了。
建議:應該有某種方法來阻止最初對 __next__() 的呼叫,或者用帶有指定值的 send() 呼叫來代替,其目的是支援使用封裝生成器,以便自動執行最初的 __next__()。
決議:超出提案範圍。此類生成器不應與 yield from 一起使用。
建議:如果關閉子迭代器引發帶值的 StopIteration,則將該值從對委託生成器的 close() 呼叫中返回。
此功能的動機是,可以透過關閉生成器來通知傳送到生成器的一系列值的結束。生成器將捕獲 GeneratorExit,完成其計算並返回一個結果,該結果隨後將成為 close() 呼叫的返回值。
決議:這種 close() 和 GeneratorExit 的用法將與它們目前作為緊急退出和清理機制的作用不相容。它將要求在關閉委託生成器時,子生成器關閉後,恢復委託生成器,而不是重新引發 GeneratorExit。但這不能接受,因為它無法確保在出於清理目的呼叫 close() 的情況下,委託生成器得到正確終結。
向消費者發出值結束訊號最好透過其他方式解決,例如傳送一個哨兵值或丟擲一個生產者和消費者商定的異常。然後,消費者可以檢測到哨兵或異常,並透過完成計算並正常返回來響應。這種方案在委託存在時也能正確執行。
建議:如果 close() 不返回一個值,那麼如果發生帶有非 None 值的 StopIteration,則引發一個異常。
決議:沒有明確的理由這樣做。在 Python 的其他任何地方,忽略返回值都不被視為錯誤。
批評
根據此提案,yield from 表示式的值將以與普通 yield 表示式非常不同的方式派生。這表明不包含 yield 一詞的其他語法可能更合適,但目前尚未提出可接受的替代方案。被拒絕的替代方案包括 call、delegate 和 gcall。
有人建議在子生成器中使用 return 以外的機制來確定 yield from 表示式返回的值。然而,這將干擾將子生成器視為可暫停函式的目標,因為它將無法像其他函式一樣返回值。
使用異常來傳遞返回值被批評為“濫用異常”,但沒有對此主張給出具體理由。無論如何,這只是一種建議的實現;可以使用另一種機制,而不會丟失提案的任何基本特性。
有人建議使用不同的異常,例如 GeneratorReturn,而不是 StopIteration 來返回值。然而,尚未提出令人信服的實際理由,並且為 StopIteration 新增 value 屬性減輕了從可能或可能沒有返回值的 StopIteration 異常中提取返回值的任何困難。此外,使用不同的異常意味著,與普通函式不同,生成器中沒有值的“return”將不等於“return None”。
替代方案
以前也曾提出過類似的提案,有些使用 yield * 而不是 yield from 語法。yield * 雖然更簡潔,但可以說它看起來與普通的 yield 太相似,在閱讀程式碼時可能會被忽略。
據作者所知,以前的提案只關注生成值,因此受到批評,認為它們所取代的兩行 for 迴圈寫起來並不夠麻煩,不足以證明一種新語法的合理性。透過處理完整的生成器協議,本提案提供了更多的益處。
附加材料
提供了一些關於所提議語法用法的示例,以及基於上述第一個最佳化的原型實現。
針對 Python 3.3 更新的實現版本可從追蹤器 issue #11682 獲取
版權
本文件已置於公共領域。
來源: https://github.com/python/peps/blob/main/peps/pep-0380.rst
最後修改: 2025-02-01 08:59:27 GMT