[譯]PEP 342--增強型生成器:協程

weixin_33716557發表於2019-01-27
4021-c953a8fb778b6948.jpg
adorable-animal-animal-photography-978555.jpg

PEP原文 : https://www.python.org/dev/peps/pep-0342/

PEP標題: Coroutines via Enhanced Generators

PEP作者: Guido van Rossum, Phillip J. Eby

建立日期: 2005-05-10

合入版本: 2.5

譯者豌豆花下貓Python貓 公眾號作者)

目錄

  • 簡介
  • 動機
  • 規格摘要
  • 規格:將值傳送到生成器
    • 新的生成器方法:send(value)
    • 新的語法:yield 表示式
  • 規格:異常和清理
    • 新語法:yield 允許在try-finally
    • 新的生成器方法:throw(type,value = None,traceback = None)
    • 新的標準異常:GeneratorExit
    • 新的生成器方法:close()
    • 新的生成器方法:__del__()
  • 可選的擴充套件
    • 擴充套件的 continue 表示式
  • 未決問題
  • 示例
  • 參考實現
  • 致謝
  • 參考文獻
  • 版權

簡介

這個 PEP 在生成器的 API 和語法方面,提出了一些增強功能,使得它們可以作為簡單的協程使用。這基本上是將下述兩個 PEP 的想法結合起來,如果它被採納,那它們就是多餘的了:

  • PEP-288,關於生成器的屬性特徵與異常(Attributes and Exceptions)。當前 PEP 沿用了它的下半部分,即生成器的異常(事實上,throw() 的方法名就取自 PEP-288)。PEP-342 用 yield 表示式(這個概念來自 PEP-288 的早期版本)來替換了生成器的屬性特徵。
  • PEP-325,生成器支援釋放資源。PEP-342 收緊了 PEP-325 中的一些鬆散的規範,使其更適用於實際的實現。

(譯註:PEP-288 和 PEP-325 都沒有被採納通過,它們的核心內容被整合到了 PEP-342裡。)

動機

協程是表達許多演算法的自然方式,例如模擬/模擬、遊戲、非同步 I/O、以及其它事件驅動程式設計或協同的多工處理。Python 的生成器函式幾乎就是協程——但不完全是——因為它們允許暫停來生成值,但又不允許在程式恢復時傳入值或異常。它們也不允許在 try-finally 結構的 try 部分作暫停,因此很難令一個異常退出的(aborted)協程來清理自己。

同樣地,當其它函式在執行時,生成器不能提供控制,除非這些函式本身是生成器,並且外部生成器之所以寫了去 yield,是要為了響應內部生成器所 yield 的值。這使得即使是相對簡單的實現(如非同步通訊)也變得複雜,因為呼叫任意函式,要麼需要生成器變堵塞(block,即無法提供控制),要麼必須在每個要呼叫的函式的周圍,新增一大堆引用迴圈程式碼(a lot of boilerplate looping code)。

但是,如果有可能在生成器掛起的點上傳遞進來值或者異常,那麼,一個簡單的協程排程器或蹦床函式(trampoline function)就能使協程相互呼叫且不用阻塞——對非同步應用程式有巨大好處。這些應用程式可以編寫協程來執行非阻塞的 socket I/O,通過給 I/O 排程器提供控制,直到資料被髮送或變為可用。同時,執行 I/O 的程式碼只需像如下方式操作,就能暫停執行,直到 nonblocking_read() 繼續產生一個值:

data = (yield nonblocking_read(my_socket, nbytes))

換句話說, 通過給語言和生成器型別增加一些相對較小的增強,Python 不需要為整個程式編寫一系列回撥,就能支援非同步操作,並且對於本該需要數百上千個協作式的多工偽執行緒的(co-operatively multitasking pseudothreads)程式,也可以不需要使用資源密集型執行緒。因此,這些增強功能將給標準 Python 帶來 Stackless Python 的許多優點,又無需對 CPython 核心及其 API 進行任何重大的修改。此外,這些增強在任何已經支援生成器的 Python 實現(例如 Jython)上都是可落實的。

規格摘要

通過給生成器型別增加一些簡單的方法,以及兩個微小的語法調整,Python 開發者就能夠使用生成器函式來實現協程與其它的協作式多工。這些方法和調整是:

  1. 重定義 yield 為表示式(expression),而不是語句(statement)。當前的 yield 語句將變成一個 yield 表示式,其值將被丟棄。每當通過正常的 next() 呼叫來恢復生成器時,yield 表示式的返回值是 None。
  2. 為生成器(generator-iterator)新增一個新的 send() 方法,它會恢復生成器,並且 send 一個值作為當前表示式的結果。send() 方法返回的是生成器產生的 next 值,若生成器沒有產生值就退出的話,則丟擲 StopIteration
  3. 為生成器(generator-iterator)新增一個新的 throw() 方法,它在生成器暫停處丟擲異常,並返回生成器產生的下一個值,若生成器沒有產生值就退出的話,則丟擲 StopIteration (如果生成器沒有捕獲傳入的異常,或者它引發了其它異常,則該異常會傳遞給呼叫者。)
  4. 為生成器(generator-iterator)新增一個新的 close() 方法,它在生成器暫停處引發 GeneratorExit 。如果生成器在之後引發 StopIteration (通過正常退出,或者已經被關閉)或 GeneratorExit (通過不捕獲異常),則 close() 返回給其呼叫者。如果生成器產生一個值,則丟擲 RuntimeError。如果生成器引發任何其它異常,也會傳遞給呼叫者。如果生成器已經退出(異常退出或正常退出),則 close() 不執行任何操作。
  5. 增加了支援,確保即使在生成器被垃圾回收時,也會呼叫 close()。
  6. 允許 yield 在 try-finally 塊中使用,因為現在允許在 finally 語句中執行垃圾回收或顯式地呼叫 close() 。

實現了所有這些變更的原型補丁已經可用了,可作為當前 Python CVS HEAD 的 SourceForge 補丁。# 1223381

設計規格:將值傳送進生成器

新的生成器方法:send(value)

為生成器提出了一種新的方法,即 send() 。它只接收一個引數,並將它傳送給生成器。呼叫 send(None) 完全等同於呼叫生成器的 next() 方法。使用其它引數呼叫 send() 也有同樣的效果,不同的是,當前生成器表示式產生的值會不一樣。

因為生成器在生成器函式體的頭部執行,所以在剛剛建立生成器時不會有 yield 表示式來接收值,因此,當生成器剛啟動時,禁止使用非 None 引數來呼叫 send() ,如果呼叫了,就會丟擲 TypeError (可能是由於某種邏輯錯誤)。所以,在與協程通訊前,必須先呼叫 next() 或 send(None) ,來將程式推進到第一個 yield 表示式。

與 next() 方法一樣,send() 方法也返回生成器產生的下一個值,或者丟擲 StopIteration 異常(當生成器正常退出,或早已退出時)。如果生成器出現未捕獲的異常,則它會傳給呼叫者。

新語法:yield 表示式

yield 語句(yield-statement)可以被用在賦值表示式的右側;在這種情況下,它就是 yield 表示式(yield-expression)。除非使用非 None 引數呼叫 send() ,否則 yield 表示式的值就是 None。見下文。

yield 表示式必須始終用括號括起來,除非它是作為頂級表示式而出現在賦值表示式的右側。所以,下面例子都是合法的:

x = yield 42
x = yield
x = 12 + (yield 42)
x = 12 + (yield)
foo(yield 42)
foo(yield)

而下面的例子則是非法的(舉了一些特例的原因是,當前的 yield 12,42 是合法的):

x = 12 + yield 42
x = 12 + yield
foo(yield 42, 12)
foo(yield, 12)

請注意,如今沒有表示式的 yield-語句 和 yield-表示式是合法的。這意味著:當 next() 呼叫中的資訊流被反轉時,應該可以在不傳遞顯式的值的情況下 yield (yield 當然就等同於 yield None)。

當呼叫 send(value) 時,它恢復的 yield 表示式將返回傳入的值。當呼叫 next() 時,它恢復的 yield 表示式將返回 None。如果 yield-表示式(yield-expression)是一個 yield-語句(yield-statement),其返回值會被忽略,就類似於忽略用作語句的函式的返回值。

實際上,yield 表示式就像一個反函式呼叫(inverted function);它所 yield 的值實際上是當前函式返回(生成)的,而它 return 的值則是通過 send() 傳入的引數。

提示:這樣的擴充語法,使得它非常地接近於 Ruby。這是故意的。請注意,Python 在阻塞時,通過使用 send(EXPR) 而不是 return EXPR 來傳值給生成器,並且在生成器與阻塞之間傳遞控制權的底層機制完全不同。Python 中的阻塞不會被編譯成 thunk,相反,yield 暫停生成器的執行進度。有一些不是這樣的特例,在 Python 中,你不能儲存阻塞以供後續呼叫,並且你無法測試是否存在著阻塞。(XXX - 關於阻塞的這些東西似乎不合適,或許 Guido 會編輯下,做澄清。)

設計規格:異常和清理

讓生成器物件成為通過呼叫生成器函式而生成的迭代器。本節中的 g 指的都是生成器物件。

新語法:yield 允許在 try-finally 裡

生成器函式的語法被擴充了,允許在 try-finally 語句中使用 yield 語句。

新的生成器方法:throw(type,value = None,traceback = None)

g.throw(type, value, traceback) 會使生成器在掛起的點處丟擲指定的異常(即在 yield 語句中,或在其函式體的頭部、且還未呼叫 next() 時)。如果生成器捕獲了異常,並生成了新的值,則它就是 g.throw() 的返回值。如果生成器沒有捕獲異常,那 throw() 也會丟擲同樣的異常(它溜走了)。如果生成器丟擲其它異常(包括返回時產生的 StopIteration),那該異常會被 throw() 丟擲。總之,throw() 的行為類似於 next() 或 send(),除了它是在掛起點處丟擲異常。如果生成器已經處於關閉狀態,throw() 只會丟擲經過它的異常,而不去執行生成器的任何程式碼。

丟擲異常的效果完全像它所宣告的那樣:

raise type, value, traceback

會在暫停點執行。type 引數不能是 None,且 type 與 value 的型別必須得相容。如果 value 不是 type 的例項(instance),則按照 raise 語句建立異常例項的規則,用 value 來生成新的異常例項。如果提供了 traceback 引數,則它必須是有效的 Python 堆疊(traceback)物件,否則會丟擲 TypeError 。

註釋:選擇 throw() 這個名稱,有幾個原因。Raise 是一個關鍵字,因此不能作為方法的名稱。與 raise 不同(它在執行點處即時地丟擲異常),throw() 首先恢復生成器,然後才丟擲異常。單詞 throw 意味著將異常拋在別處,並且跟其它語言相關聯。

考慮了幾個替代的方法名:resolve(), signal(), genraise(), raiseinto()flush() 。沒有一個像 throw() 那般合適。

新的標準異常:GeneratorExit

定義了一個新的標準異常 GeneratorExit,繼承自 Exception。生成器應該繼續丟擲它(或者就不捕獲它),或者通過丟擲 StopIteration 來處理這個問題。

新的生成器方法:close()

g.close() 由以下虛擬碼定義:

def close(self):
    try:
        self.throw(GeneratorExit)
    except (GeneratorExit, StopIteration):
        pass
    else:
        raise RuntimeError("generator ignored GeneratorExit")
    # Other exceptions are not caught

新的生成器方法:__del__()

g.__ del __() 是 g.close() 的裝飾器。當生成器物件被作垃圾回收時,會呼叫它(在 CPython 中,則是它的引用計數變為零時)。如果 close() 引發異常, 異常的堆疊資訊(traceback)會被列印到 sys.stderr 並被忽略掉;它不會退回到觸發垃圾回收的地方。這與類例項在處理 __del__()的異常時的方法一樣。

如果生成器物件被迴圈引用,則可能不會呼叫 g.__del__() 。這是當前 CPython 的垃圾收集器的表現。做此限制的原因是,GC 程式碼需要在一個任意點打破迴圈,以便回收它,在此之後,不允許 Python 程式碼“看到”形成迴圈的物件,因為它們可能處於無效的狀態。被用於解開(hanging off)迴圈的物件不受此限制。

儘管實際上不太可能看到生成器被迴圈引用。但是,若將生成器物件儲存在全域性變數中,則會通過生成器框架的 f_globals 指標建立一個迴圈。另外,若在資料結構中儲存對生成器物件的引用,且該資料結構被作為引數傳遞給生成器,這也會創造一個迴圈引用(例如,如果一個物件具有一個作為生成器的方法,並持有由該方法建立的執行中的迭代器的引用)。鑑於生成器的典型用法,這些情況都不太可能。

此外,CPython 在實現當前 PEP 時,每當由於錯誤或正常退出而終止執行時,會釋放被生成器使用的框架物件(frame object)。這保證了那些無法被恢復的生成器不會成為無法回收的迴圈引用的部分。這就允許了其它程式碼在 try-finally 或 with 語句中使用 close() (參考 PEP-343),確保了給定的生成器會正確地完結。

可選擴充套件

擴充套件的 continue 語句

本 PEP 的早期草案提出了一種新的 continue EXPR 語法,用於 for 迴圈(繼承自 PEP-340),將 EXPR 的值傳給被遍歷的迭代器。此功能暫時被撤銷了,因為本 PEP 的範圍已經縮小,只關注將值傳給生成器迭代器(generator-iterator),而非其它型別的迭代器。Python-Dev 郵件列表中的一些人也覺得為這個特定功能新增新語法是為時過早(would be premature at best)。

未決問題

Python-Dev 郵件的討論提出了一些未決的問題。我羅列於此,附上我推薦的解決方案與它的動機。目前編寫的 PEP 也反映了這種喜好的解決方案。

  1. 當生成器產生另一個值作為對“GeneratorExit”異常的響應時,close()應該引發什麼異常?

    我最初選擇了 TypeError ,因為它表示生成器函式發生了嚴重的錯誤行為,應該通過修改程式碼來修復。但是 PEP-343 中的 with_template 裝飾器類使用了 RuntimeError 來進行類似處理。可以說它們都應該使用相同的異常。我寧願不為此目的引入新的異常類,因為它不是我希望人們捕獲的異常:我希望它變成一個 traceback 給程式設計師看到,然後進行修復。所以我覺得它們都應該丟擲 RuntimeError 。有一些先例:在檢測到無限遞迴的情況下,或者檢測到未初始化的物件(由於各種各樣的原因),核心 Python 程式碼會丟擲該異常。

  2. Oren Tirosh 建議將 send() 方法重新命名為 feed() ,以便能跟 consumer 介面相容(規範參見:http://effbot.org/zone/consumer.htm)。

然而,仔細觀察 consumer 介面,似乎 feed() 所需的語義與 send() 不同,因為後者不能在剛啟動的生成器上作有意義的呼叫。此外,當前定義的 consumer 介面不包含對 StopIteration 的處理。

因此,建立一個貼合 consumer 介面的簡單的裝飾器,來裝飾生成器函式,似乎會更有用。舉個例子,它可以用初始的 next() 呼叫給生成器預熱(warm up),追蹤 StopIteration,甚至可以通過重新呼叫生成器來提供 reset() 用途。

示例

  1. 一個簡單的 consumer 裝飾器,它使生成器函式在最初呼叫時,就自動地前進到第一個 yield 點:
def consumer(func):
    def wrapper(*args,**kw):
        gen = func(*args, **kw)
        gen.next()
        return gen
    wrapper.__name__ = func.__name__
    wrapper.__dict__ = func.__dict__
    wrapper.__doc__  = func.__doc__
    return wrapper
  1. 一個使用 consumer 裝飾器建立反向生成器(reverse generator)的示例,該生成器接收影像並建立縮圖,再傳送給其它 consumer。像這樣的函式可以連結在一起,形成 consumer 間的高效處理流水線,且每個流水線都可以具有複雜的內部狀態:
@consumer
def thumbnail_pager(pagesize, thumbsize, destination):
    while True:
        page = new_image(pagesize)
        rows, columns = pagesize / thumbsize
        pending = False
        try:
            for row in xrange(rows):
                for column in xrange(columns):
                    thumb = create_thumbnail((yield), thumbsize)
                    page.write(
                        thumb, col*thumbsize.x, row*thumbsize.y )
                    pending = True
        except GeneratorExit:
            # close() was called, so flush any pending output
            if pending:
                destination.send(page)

            # then close the downstream consumer, and exit
            destination.close()
            return
        else:
            # we finished a page full of thumbnails, so send it
            # downstream and keep on looping
            destination.send(page)

@consumer
def jpeg_writer(dirname):
    fileno = 1
    while True:
        filename = os.path.join(dirname,"page%04d.jpg" % fileno)
        write_jpeg((yield), filename)
        fileno += 1


# Put them together to make a function that makes thumbnail
# pages from a list of images and other parameters.
#
def write_thumbnails(pagesize, thumbsize, images, output_dir):
    pipeline = thumbnail_pager(
        pagesize, thumbsize, jpeg_writer(output_dir)
    )

    for image in images:
        pipeline.send(image)

    pipeline.close()
  1. 一個簡單的協程排程器或蹦床(trampoline),它允許協程通過 yield 其它協程,來呼叫後者。被呼叫的協程所產生的非生成器的值,會被返回給呼叫方的協程。類似地,如果被呼叫的協程丟擲異常,該異常也會傳導給呼叫者。實際上,只要你用 yield 表示式來呼叫協程(否則會阻塞),這個例子就模擬了 Stackless Python 中使用的簡單的子任務(tasklet)。這只是一個非常簡單的例子,但也可以使用更復雜的排程程式。(例如,現有的 GTasklet 框架 (http://www.gnome.org/~gjc/gtasklet/gtasklets.html) 和 peak.events 框架 (http://peak.telecommunity.com/) 已經實現類似的排程功能,但大多數因為無法將值或異常傳給生成器,而必須使用很尷尬的解決方法。)
import collections

class Trampoline:
    """Manage communications between coroutines"""

    running = False

    def __init__(self):
        self.queue = collections.deque()

    def add(self, coroutine):
        """Request that a coroutine be executed"""
        self.schedule(coroutine)

    def run(self):
        result = None
        self.running = True
        try:
            while self.running and self.queue:
               func = self.queue.popleft()
               result = func()
            return result
        finally:
            self.running = False

    def stop(self):
        self.running = False

    def schedule(self, coroutine, stack=(), val=None, *exc):
        def resume():
            value = val
            try:
                if exc:
                    value = coroutine.throw(value,*exc)
                else:
                    value = coroutine.send(value)
            except:
                if stack:
                    # send the error back to the "caller"
                    self.schedule(
                        stack[0], stack[1], *sys.exc_info()
                    )
                else:
                    # Nothing left in this pseudothread to
                    # handle it, let it propagate to the
                    # run loop
                    raise

            if isinstance(value, types.GeneratorType):
                # Yielded to a specific coroutine, push the
                # current one on the stack, and call the new
                # one with no args
                self.schedule(value, (coroutine,stack))

            elif stack:
                # Yielded a result, pop the stack and send the
                # value to the caller
                self.schedule(stack[0], stack[1], value)

            # else: this pseudothread has ended

        self.queue.append(resume)
  1. 一個簡單的 echo 伺服器以及用蹦床原理實現的執行程式碼(假設存在 nonblocking_readnonblocking_write 和其它 I/O 協程,該例子在連線關閉時丟擲 ConnectionLost ):
# coroutine function that echos data back on a connected
# socket
#
def echo_handler(sock):
    while True:
        try:
            data = yield nonblocking_read(sock)
            yield nonblocking_write(sock, data)
        except ConnectionLost:
            pass  # exit normally if connection lost

# coroutine function that listens for connections on a
# socket, and then launches a service "handler" coroutine
# to service the connection
#
def listen_on(trampoline, sock, handler):
    while True:
        # get the next incoming connection
        connected_socket = yield nonblocking_accept(sock)

        # start another coroutine to handle the connection
        trampoline.add( handler(connected_socket) )

# Create a scheduler to manage all our coroutines
t = Trampoline()

# Create a coroutine instance to run the echo_handler on
# incoming connections
#
server = listen_on(
    t, listening_socket("localhost","echo"), echo_handler
)

# Add the coroutine to the scheduler
t.add(server)

# loop forever, accepting connections and servicing them
# "in parallel"
#
t.run()

參考實現

實現了本 PEP 中描述的所有功能的原型補丁已經可用,參見 SourceForge 補丁 1223381 (https://bugs.python.org/issue1223381)。

該補丁已提交到 CVS,2005年8月 01-02。

致謝

Raymond Hettinger (PEP 288) 與 Samuele Pedroni (PEP 325) 第一個正式地提出將值或異常傳遞給生成器的想法,以及關閉生成器的能力。Timothy Delaney 建議了本 PEP 的標題,還有 Steven Bethard 幫忙編輯了早期的版本。另見 PEP-340 的致謝部分。

參考文獻

TBD.

版權

本文件已經放置在公共領域。

源文件:https://github.com/python/peps/blob/master/pep-0342.txt

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

相關連結:

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

PEP翻譯計劃https://github.com/chinesehuazhou/peps-cn

[譯] PEP 255--簡單的生成器

[譯]PEP 525--非同步生成器

花下貓語: 嘮叨幾句吧,年前這幾周事情太多了,擠著時間好歹是又翻譯出一篇 PEP。與生成器密切相關的 PEP 已經完成 3/4,年後再譯最後一篇(PEP-380)。當初翻譯第一篇,完全是一時興起,直覺這是一件有意義的事,現在呢,這個念頭開始有點膨脹——我竟然在 Github 上建了個翻譯專案。我深知,自己水平實在有限,因此不求得到多少認同吧。但行好事,莫問前程。不過,若有人幫著吆喝一聲,也是極好的。

-----------------

本文原創並首發於微信公眾號【Python貓】,後臺回覆“愛學習”,免費獲得20+本精選電子書。

相關文章