[譯]PEP 380--子生成器的語法

導語: PEP(Python加強提案)幾乎是 Python 社區中最重要的文檔,它們提供了公告信息、指導流程、新功能的設計及使用說明等內容。對於學習者來講,PEP 是很是值得一讀的第一手材料,學習中遇到的大部分難題,都能在 PEP 中找到答案或者解決思路。html

我翻譯了幾篇 PEP,這麼作的目的一方面是爲了增強學習,另外一方面也是爲了鍛鍊本身的英文水平。Python 與 English,都是如此重要。翻譯能將二者巧妙地結合起來,真是一箭雙鵰。python

本文介紹了子生成器的語法,即 yield from 語法。其它與生成器相關的 PEP 有 3 篇,翻譯的結果附在了本文末尾。如有對翻譯感興趣的同窗,可在 Github 上關注下我建立的項目 peps-cngit


PEP原文 : https://www.python.org/dev/pe...github

PEP標題: Syntax for Delegating to a Subgenerator異步

PEP做者: Gregory Ewing函數

建立日期: 2009-02-13學習

合入版本: 3.3優化

譯者豌豆花下貓Python貓 公衆號做者)ui

目錄

  • 摘要
  • PEP接受
  • 動機
  • 提議spa

    • StopIteration 的加強
    • 形式語義
  • 基本原理

    • 重構原則
    • 結束方式
    • 做爲線程的生成器
    • 語法
    • 優化
    • 使用StopIteration來返回值
    • 被拒絕的建議
  • 批評
  • 可選的提案
  • 附加材料
  • 參考資料
  • 版權

摘要

爲生成器提出了一種新的語法,用於將部分的操做委派給其它的生成器。這使得一部分包含「yield」的代碼段,能夠被分離並放置到其它生成器中。與此同時,子生成器會返回一個值,交給委派生成器(delegating generator)使用。

當一個生成器再次 yield 被另外一個生成器生成的值時,該語法還創造了一些優化的可能。

PEP接受

Guido 於 2011 年 6 月 26 日正式接受本 PEP。

動機

Python 的生成器是一種協程,但有一個限制,它只能返回值給直接的調用者。這意味着包含了 yield 的代碼段不能像其它代碼段同樣,被拆分並放入到單獨的函數中。若是作了這樣的分解,就會致使被調用的函數自己成爲一個生成器,而且必須顯式地迭代這個生成器,以便從新 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() 。

子迭代器(而非生成器)的語義被選擇成爲生成器案例的合理泛化(generalization)。

所提出的語義在重構方面具備以下限制:

  • 一個捕獲了 GenetatorExit 卻不從新拋出的代碼塊,不能在徹底保留相同行爲的狀況下被分離出去。
  • 若是將 StopIteration 異常拋進了委託生成器中,則分離的生成器的行爲跟原始代碼的行爲可能會不一樣。

因爲這些用例幾乎不存在,所以不值得爲支持它們而考慮額外的複雜性。

結束方式

當在 yield from 處掛起時,而且使用 close() 方法顯式地終止委託生成器時,關因而否要一併終止子迭代器,存在一些爭議。一個反對的論據是,若是在別處存在對子迭代器的引用,這樣作會致使過早結束它。

對非引用計數型的 Python 實現的考慮,致使了應該顯式地結束的結論,以便在全部類型的 Python 實現上,顯式地結束子迭代器與非重構的迭代器,能具備相同的效果。

這裏作的假設是,在大多數用例中,子迭代器不會被共享。在子迭代器被共享的稀有狀況下,可經過一個阻塞調用 throw() 和 close() 的裝飾器來實現,或者使用除 yield from 之外的方法來調用子迭代器。

做爲線程的生成器

使生成器可以 return 值的動機,還考慮到使用生成器來實現輕量級的線程。當以這種方式使用生成器時,將輕量級線程的計算擴散到許多函數上就會是合理的。人們但願可以像調用普通函數同樣調用子生成器,傳遞給它參數並接收返回值。

使用提議的語法,像如下的表達式

y = f(x)

其中 f 是一個普通的函數,就能夠被轉化成一個委託調用

y = yield from g(x)

其中 g 是生成器。經過把 g 想象成一個普通的能被 yield 語句掛起的函數,人們能夠推斷出結果代碼的行爲。

當以這種方式把生成器做爲線程使用時,一般人們不會對 yield 所傳入或傳出的值感興趣。可是,也有一些例子,線程能夠做爲 item 的生產者或消費者。yield from 表達式容許線程的邏輯被擴散到所需的儘量多的函數中,item 的生產與消費發生在任意的子函數中,而且這些 item 會自動路由到/去它們的最終來源/目的地。

對於 throw()close() ,能夠合理地預期,若是從外部向線程內拋入了一個異常,那麼首先應該在線程掛起處的最內部的生成器中引起,再從那裏向外傳遞;而若是線程是從外部調用 close() 來終結的,那也應該從最內部往外地終止處於活動態的生成器鏈。

語法

所提出的特定語法被選中,像它的含義所暗示,並無引入任何新的關鍵詞,且清晰地突出了它與普通 yield 的不一樣。

優化

當存在一長串生成器時,使用專門的語法就爲優化提供了可能性。這種生成器鏈可能存在,例如,當遞歸遍歷樹結構時。在鏈上傳遞 __next__() 的調用與 yield 返回值,可能形成 O(n) 開銷,最壞狀況下會是 O(n**2)。

可能的策略是向生成器對象添加一個槽(slot)來保存委派給它的生成器。當在生成器上調用 __next__() 或 send() 時,首先檢查該槽,若是非空,則它引用的生成器將會被激活。若是引起了 StopIteration,該槽會被清空,而且主生成器會被激活。

這將減小一系列 C 函數調用的委託開銷,並不涉及 Python 代碼的執行。一種可能的加強方法是在循環中遍歷整個生成器鏈,並直接激活最後一個生成器,儘管 StopIteration 的處理會比較複雜。

使用StopIteration來返回值

有多種方法能夠將生成器的返回值傳回。也有一些替代的方法,例如將其存儲爲生成器-迭代器對象的屬性,或將其做爲子生成器的 close() 方法的調用值返回。然而,本 PEP 提議的機制頗有吸引力,有以下理由:

  • 使用泛化的 StopIteration 異常,可使其它類型的迭代器輕鬆地加入協議,而沒必要增長額外的屬性或 close() 方法。
  • 它簡化了實現,由於子生成器的返回值變得可用的點與引起異常的點相同。延遲到任意時間都須要在某處存儲返回值。

被拒絕的建議

一些想法被討論而且拒絕了。

建議:應該有一些方法能夠避免對__next__() 的調用,或者用帶有指定值的 send() 調用來替換它,目的是支持對生成器做裝飾,以即可以自動地執行初始的 __next__()

決議:超出本提案的範圍。這種生成器不應與 yield from 一塊兒使用。

建議:若是關閉一個子迭代器時,引起了帶返回值的 StopIteration 異常,則將該值從 close() 調用中返回給委託生成器。

此功能的動機是爲了經過關閉生成器,傳信號給傳入生成器的最後的值。被關閉的生成器會捕獲 GeneratorExit ,完成其計算並返回一個結果,該結果最終成爲 close() 調用的返回值。

決議:close() 與 GeneratorExit 的這種用法,將與當前的退出(bail-out)與清理機制的角色不兼容。這要求在關閉子生成器後、關閉一個委託生成器時,該委託生成器能夠被恢復,而不是從新引起 GeneratorExit。但這是不可接受的,由於調用 close() 進行清理的意圖,沒法保證委託生成器能正確地終止。

經過其它方式,能夠更好地處理向消費者告知(signal)最後的值的問題,例如發送一個哨兵值(sentinel value)或者拋入一個被生產者與消費者都承認的異常。而後,消費者能夠檢查該哨兵或異常,經過完成其計算並正常地返回,來做響應。這種方案在存在委託的狀況下表現正確。

建議:若是 close() 不返回值,若是出現 StopIteration 中帶有非 None 的值,則拋出一個異常。

決議:沒有明確的理由如此作。忽略返回值在 Python 中的任何其它地方,都不會被視爲錯誤。

批評

根據本提案,yield from 表達式的值將以跟普通 yield 表達式很是不一樣的方式得出。這意味着其它不包含 yield 表達式的語法可能會更合適,但到目前爲止,尚未提出可接受的替代方案。被拒絕的替代品包括 call、delegate 和 gcall。

有人提議,應該使用子生成器中除 return 之外的某些機制,來處理 yield from 表達式的返回值。可是,這會干擾將子生成器視爲可掛起函數的目的,由於它不能像其它函數同樣 return 值。

有人批評,說使用異常來傳遞返回值是「濫用異常」,卻沒有任何具體的理由來證實它。不管如何,這只是一種實現的建議;其它機制能夠在不丟失本提案的任何關鍵特性的狀況下使用。

有人建議,使用與 StopIteration 不一樣的異常來返回值,例如 GeneratorReturn。可是,尚未使人信服的實際理由被提出,而且向 StopIteration 添加 value 屬性減輕了從異常(該異常可能存在也可能不存在)中提取返回值的全部困難。此外,使用不一樣的異常意味着,與普通函數不一樣,生成器中不帶值的 return,將不等同於 return None

可選的提案

以前已經提到了相似的提議,有些語法使用 yield 而不是 yield from。雖然 yield 更簡潔,可是有爭議的是,它看起來與普通的 yield 太類似了,可能在閱讀代碼時會忽視了其中的差別。

據做者所知,以前的提案只關注於 yield 產生值,所以遭受到了批評,即他們所替代的兩行 for 循環並無足夠使人厭煩,不足以讓人爲新的語法辯護。經過處理完整的生成器協議,本提案提供了更多的好處。

附加材料

本提案的語法的一些用例已經被提供出來,而且基於上面歸納的第一個優化的原型也已實現。

Examples and Implementation

能夠從跟蹤器問題的 issue 11682 中得到針對 Python 3.3 實現的升級版本。

參考資料

[1] https://mail.python.org/pipermail/python-dev/2011-June/112010.html

[2] http://www.cosc.canterbury.ac.nz/greg.ewing/python/yield-from/

[3] http://bugs.python.org/issue11682

版權

本文檔已經放置在公共領域。源文檔:

https://github.com/python/pep...

-------------(譯文完)-------------

相關連接:

PEP背景知識學習Python,怎能不懂點PEP呢?

PEP翻譯計劃https://github.com/chinesehua...

[[譯] PEP 255--簡單的生成器](https://mp.weixin.qq.com/s/vj...

[[譯] PEP 342--加強型生成器:協程](https://mp.weixin.qq.com/s/M7...

[[譯] PEP 525--異步生成器](https://mp.weixin.qq.com/s/fy...


公衆號【Python貓】, 專一Python技術、數據科學和深度學習,力圖創造一個有趣又有用的學習分享平臺。本號連載優質的系列文章,有喵星哲學貓系列、Python進階系列、好書推薦系列、優質英文推薦與翻譯等等,歡迎關注哦。PS:後臺回覆「愛學習」,免費得到一份學習大禮包。

相關文章
相關標籤/搜索