Previous topic

私家版 日本語 PEP 集

Next topic

3119 抽象基底クラスの導入

This Page

342 機能強化したジェネレータによるコルーチン

  • PEP: 342
  • Title: Coroutines via Enhanced Generators
  • Version: $Revision: 45616 $
  • Last-Modified: $Date: 2006-04-21 17:22:45 +0200 (Fri, 21 Apr 2006) $
  • Author: Guido van Rossum, Phillip J. Eby
  • Status: Final
  • Type: Standards Track
  • Content-Type: text/plain
  • Created: 10-May-2005
  • Post-History:

はじめに

この PEP では単純なコルーチンとして使えるようにジェネレータの API と文法を強化するいくつかの提案をします。 内容は基本的に以下の二つの PEP のアイディアを組合せたものです:

  • PEP 288, Generators Attributes and Exceptions. これから説明する PEP は PEP 288 の後半のジェネレータ例外とかぶります (というか throw() メソッドは PEP 288 から採りました)。 ジェネレータ属性の置き換えの方は、PEP 288 の昔のリビジョンである “yield expression” からの考え方に対するものです。
  • PEP 325, Resource-Release Support for Generators. PEP 342 は PEP 325 仕様を実際の実装に合うように片付けたものです。

これらはこの提案が通れば冗長なものと見なされるでしょう。

動機

コルーチンは多くのアルゴリズムを記述する自然な方法です。 たとえば、シミュレーション、ゲーム、非同期 I/O その他のイベント・ドリブンなものや、 協調的マルチタスクなどのアルゴリズムです。 Python のジェネレータ関数はほぼコルーチンと言えるでしょう。 しかし足りない部分があります。 値を作り出して実行を一旦休止することができます、が、それを再開するときに値もしくは例外を受け取る方法はありません。 もう一つ try/finally ブロックの中で実行を休止することもできませんので、 中断されたコルーチンの後始末も容易ではありません。

また、ジェネレータは他の関数が実行している間に制御を明け渡すことは普通できません。 できるとすれば、その関数というのがそれ自身ジェネレータになっていて、 外側のジェネレータが内側のジェネレータの yield する値に呼応して yield するように書かれている場合でしょう。 こういった状況によって、相対的に簡単なユースケースといえる非同期コミュニケーションでさえ実装が複雑になります。 というのは、どの関数の呼び出しもジェネレータを「ブロック」する(即ち制御を明け渡させないようにする)か、または関数呼び出しの周囲にたくさんのループを含んだボイラープレートを埋め込むことが必要になるからです。

しかし、もし値や例外をジェネレータ の中へ その実行が保留されているところへ渡すことができたならば、単純なコルーチン・スケジューラや「トランポリン」関数などのコルーチン同士が互いにブロックせずに「呼び出し」合えるようになり、 非同期アプリケーションに与える恩恵は計り知れません。 そうなった暁には、非同期アプリケーションは非ブロッキング・ソケット I/O をデータが送られるか使えるようになるまで I/O スケジューラに制御を明け渡すようなコルーチンとして書けることでしょう。 一方で、 I/O を担当するコードは簡単に次のようなことをします:

data = (yield nonblocking_read(my_socket, nbytes))

これで nonblocking_read() コルーチンが値を生み出すまで実行は一時停止します。

別の言い方をすると、相対的に小さな少数の機能強化を言語およびジェネレータ・イテレータ型の実装に施すことによって、アプリケーションの全体をコールバックの連鎖で書いたりする必要も、数百数千の協調的スレッドもどきを使うプログラムにリソース食いのスレッドを使わせる要求も無しに、Python にサポートさせることができます。 ですから、このいくつかの機能強化は CPython コアや API に大きな変更を加えることなく、 スタックレス Python 派の大部分の利益を標準 Python にもたらします。 さらに、この機能強化は CPython 以外の Python 実装(たとえば Jython とか) が既にジェネレータをサポートしていれば容易に実装できるはずです。

仕様の概要

ジェネレータ・イテレータ型に少数の簡単なメソッドを付け加え、 二つの些細な文法の調整を行うことで、 Python 開発者はコルーチンや他の形の協調的マルチタスク機能を実装するのにジェネレータ関数を使うことができるようになります。 そのメソッドと調整は以下の通りです:

  1. yield を文ではなく式として再定義します。 現在の yield 文は値を気にしない yield 式になります。 yield 式の値は通常の next() 呼び出しで再開される場合はいつも None です。
  2. ジェネレータ・イテレータに新しい send() メソッドを追加します。 このメソッドはジェネレータを再開させ、その際 yield 式の結果となる値を送り (send) ます。 send() メソッドが返す値はジェネレータが次に yield する値になるか、 ジェネレータが値を yield せずに終了した場合は StopIteration を送出します。
  3. ジェネレータ・イテレータに新しい throw() メソッドを追加します。 このメソッドはジェネレータの一時停止している地点で例外を発生させ、 ジェネレータが次に yield する値を返すか、 ジェネレータが値を yield せずに終了した場合は StopIteration を送出します。 (ジェネレータが送り込まれた例外を処理しなかったり、 別の例外を送出した場合には、例外が呼び出し側に伝播します。)
  4. ジェネレータ・イテレータに新しい close() メソッドを追加します。 このメソッドはジェネレータの一時停止している地点で GeneratorExit 例外を発生させます。 ジェネレータがそれを受けて StopIteration を送出する (正常終了するか、または既に停止していた)か、 GeneratorExit を送出する (例外を処理しない)場合、 close() は呼び出し側に処理を戻します。 ジェネレータが無視して次の値を yield した場合、 RuntimeError が送出されます。 これ以外の例外をジェネレータが送出した場合、それはそのまま呼び出し側に伝播します。 ジェネレータが既に終了していた場合、 close() は何もしません。
  5. ジェネレータ・イテレータがガベージ・コレクションされる際に close() が呼ばれることを保証するようにします。
  6. ガベージ・コレクションや直接的な close() の呼び出しが finally 節が実行されるのを許すことになるので、 yield を try/finally ブロック中で使えるようにします。

プロトタイプがあります。 SourceForge patch #1223381

仕様: ジェネレータへ値を送る

新しいジェネレータのメソッド: send(value)

ジェネレータ・イテレータの新しいメソッド send() を提案します。 引数が一つだけあり、この値はジェネレータに「送り込まれ」ます。 send(None) という呼び出しは next() と丸っきり同じです。 他の値を渡すのもほぼ同じことですが、ジェネレータの yield 式の値が違ってきます。

ジェネレータ・イテレータはジェネレータの関数本体の一番上から実行を開始するので、 開始直後のジェネレータには値を受け取る yield 式がありません。 したがって、ジェネレータの実行が開始された直後には None 以外の値での send() 呼び出しは禁止されます。 この規則に違反すると (おそらく何らかのロジックの間違いでしょう) TypeError が送出されます。 そういうわけで、コルーチンと通信を始める前にまず next()send(None) を呼び出して最初の yield 式まで実行を進めておく必要があります。

next() メソッドと同様に、 send() メソッドはジェネレータ・イテレータが yield する次の値を返すか、ジェネレータが終了したか既に停止している場合 StopIteration を送出します。 ジェネレータが捕捉されていない例外を送出した場合、 それはそのまま send() の呼び出し側に伝播します。

新しい文法: yield 式

yield 文が代入文の右辺で使えるようになります。 これを yield 式と呼ぶことにします。 この yield 式の値は send() で None 以外の値が送られない限りは None です。 あとでもう少し詳しく見ます。

yield 式は代入の右辺のトップレベルの式でない限りは、括弧に入れなければなりません。 次のものは全部許されます:

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

しかし、次のものは駄目です:

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

(境界ケースをこのように決めた動機は現在 yield 12, 42 と書けるところからきています)

yield 文や yield 式が式を伴わないのは文法的に正しいということに注意しておきましょう。 こう説明すればいいでしょうか。 next() 呼び出しの情報の流れが逆転したと考えた場合、 明示的な値無しに yield できるべきです (ここで yield はもちろん yield None と等価です)。

send(value) が呼び出されると、再開する yield 式はその value を返します。 next() が呼び出されると、再開する yield 式は None を返します。 yield 式が yield 文として使われる場合、この値は無視されます。 文として使う場合に関数呼び出しの値を無視するのと同じことです。

事実上、 yield 式は逆転した関数呼び出しのようなものです。 実際 yield の引数は現在実行中の関数から返され(yield され)、 yield の「返値」は send() に渡された引数です。

注意: 文法を拡張することにより yield の使い方は Ruby のそれによく似てきました。 これは意図的なものです。大事なことなので良く聞いて下さい。 Python ではブロックはジェネレータに値を渡すときに return EXPR でなく send(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) はジェネレータ g に現在保留されている地点で指定された例外を投げさせます。 (即ち、 yield 文で、またはまだ一度も next() が呼び出されていなければ関数本体の一番最初で、ということです。) ジェネレータがこの例外を捕捉して次の値を yield するなら、 その値が g.throw() の返値になります。 ジェネレータがこの例外を処理しなければ、渡した同じ例外を throw() が送出するように見えます (「抜け落ち」ます)。 ジェネレータが異なる例外 (StopIteration も含みます) を送出するならば、 この例外が throw() から送出されます。 まとめると、 throw()next()send() と同じように振る舞いますが、 保留された地点から例外を送出するという点が異なります。 ジェネレータが既に閉じた状態の場合は、ジェネレータのコードとは無関係に、 渡した例外を送出することになります。

「例外を送出する」という意味は正に以下の文が保留された地点で実行されるということです:

raise type, value, traceback

type 引数が None であってはなりません。 また typevalue は互換性がなければなりません。 valuetype のインスタンスでないならば、 value を使って新しい例外インスタンスが作られます。 これは raise 文が例外インスタンスを作り出すときと同じ規則に従います。 traceback は、もし渡されるならば、正しい Python のトレースバック・オブジェクトでなければねりません。 そうでなければ TypeError が起こります。

注意: メソッド名の throw() は幾つかの理由で選ばれました。 raise はキーワードなのでメソッド名には使えません。 raise とは違い (これはその場ですぐに例外を送出します)、 throw() はまずジェネレータを再開し、その後でしか例外を送出しません。 throw という単語は例外を別の場所に届けるという意味を伝えており、 またそれは既に他の言語における例外と結びつけられてもいます。

候補としては他に resolve(), signal(), genraise(), raiseinto(), flush() がありましたが、 throw() ほどしっくりきませんでした。

新しい標準例外: GeneratorExit

新しく標準例外として Exception を継承した GeneratorExit を定義します。 ジェネレータはこれに対応して再送出 (単に放置してもいいですが) するか、 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() のラッパーです。 このメソッドは GC される際に (CPython でいえば、参照カウントがゼロになったときです) 呼び出されます。 close() が例外を送出した場合、トレースバックが sys.stderr に印字されるだけで、 それ以上は無視されます。 つまり、GC の引き金を引いた場所には例外が戻っていったりしません。 この振る舞いはクラス・インスタンスの __del__() メソッドにおける例外の扱いと整合しています。

ジェネレータ・オブジェクトが循環参照に含まれる場合、 g.__del__() を呼び出されないかも知れません。 CPython の現在のガベージ・コレクタはこのように振る舞います。 この制限の理由は GC コードが参照の輪をどこかで「断ち切る」ことが必要で、 そのため Python コードが循環しているオブジェクトを見ることは、 それらが不正な状態にあるかも知れないので、許されないからです。 循環に「ぶら下がっている」オブジェクトにはこの制限は掛かりません。

ジェネレータ・オブジェクトが循環参照に含まれるなど現実に起こりそうもないと思われるかも知れません。 しかしながら、ジェネレータ・オブジェクトをグローバル変数に格納すると、 ジェネレータ・フレームの f_globals ポインタを通じて循環を形成します。 あるいは、ジェネレータ・オブジェクトへの参照をそのジェネレータの引数に渡すデータ構造に格納してもできます (たとえば、オブジェクトがジェネレータであるメソッドを持っていて、 そのメソッドで作られた実行中のイテレータへの参照を保持するような場合です)。 どちらもジェネレータのごく一般的な使い方というほどではありませんね。

また、この PEP の CPython での実装においては、 ジェネレータが使ったフレーム・オブジェクトは実行が、エラーによってであれ正常終了であれ、 終わったときにはいつでも解放されなければなりません。 これにより、再開の見込みがないジェネレータが回収不能な参照の循環の中に残らないことを保証します。 この保証により、他のコードが try/finally や “with” ブロック (PEP 343) の中で、 そのジェネレータが間違いなくファイナライズされることを保証するために、 close() を呼び出すことが可能になります。

選択的拡張

拡張された continue 文

この PEP の早い段階の草稿では continue EXPR という新しい文法を for ループで使うために (PEP 340 から引き継いで) 提案していました。 それは、ループに使われているイテレータに EXPR の値を渡すというものでした。 この機能はやがて引っ込められましたが、 その理由はこの PEP のスコープをジェネレータ・イテレータだけに絞ったからです。 また、python-dev ではこの特定の機能のために文法をいじるのは時期尚早との声もありました。

未解決問題

python-dev での議論の中で幾つかの未解決問題が浮かび上がりました。 ここにそのリストを示し、さらに私の良いと思った解決案とその動機を添えます。 PEP の現在の形はこの解決案を反映しています。

  1. ジェネレータが GeneratorExit 例外に対して次の値を yield してきた場合、 どの例外が close() から送出されるべきか?

    私の最初の選択は TypeError でした。 その理由は TypeError がジェネレータ関数の総体的に間違った振る舞いを表現していると考えたからです。 そのような間違いはコードを変更して修正するしかありません。 しかし PEP 343 の with_template デコレータは同じような違反行為に対し RuntimeError を使っていました。 議論の余地はありますが、どちらも同じ例外を使ってしかるべきです。 私はみんながこの例外を捕まえようとして欲しくなかったので、 この目的のために新しい例外を導入する気はありませんでした。 このエラーがトレースバックとしてプログラマーの眼前に現れ修正されて欲しいのです。 そいうことで今では私はどちらの場合も RuntimeError を送出すべきだと確信しています。 前例は既にあります。 コア Python コードは終わりの見えない再帰を検出したときや、 初期化されていないオブジェクトを見つけたときには (その他種々雑多な条件により) RuntimeError を送出しています。

  2. Oren Tirosh は send() を「消費者インタフェース」 (仕様は http://effbot.org/zone/consumer.htm を参照のこと) に合わせて feed() に改名しようとの提案をくれた。

    しかし、消費者インタフェースをよく読むと、望まれる feed() の意味論は send() のそれとは違うように思えます。 というのは、開始したばかりのジェネレータに対する send() 呼び出しは意味のあるものにならないからです。 また、消費者インタフェースは現在の定義では StopIteration の取り扱いを含みません。

    それ故に、ジェネレータ関数をラップして消費者インタフェースに適合させるような単純なデコレータを作る方がおそらく役に立つように思われます。 たとえば、最初に next() を呼び出して「暖めておく」ことができますし、 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
    
  2. 上の consumer デコレータを使い、画像を受け取ってサムネイルを作り別の消費者に送る「逆ジェネレータ(reverse generator)」の例です。 このような関数は数珠つなぎにしてそれぞれが複雑な内部状態をもつ「消費者」の処理パイプラインを構成するのに使えます:

    @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()
    
  3. 単純なコルーチン・スケジューラもしくは「トランポリン」。 コルーチンが他のコルーチンを起動したいと思うコルーチンを yield することで「呼び出す」ことができるようにします。 コルーチンが yield するジェネレータ以外の値は、 値を yield したコルーチンを「呼び出した」コルーチンに返されます。 同様に、コルーチンが例外を送出した場合にはその例外は「呼び出し側」に伝播していきます。 実際上、この例はスタックレス Python で使われる単純なタスクレットを、 yield 式をそれ無しでは「ブロック」するようなルーチンの呼び出しに使う限りにおいて、 エミュレートしたものになっています。 この例は、ごく単純な例を示したに過ぎず、もっとずっと洗練されたスケジューラを書くことも可能です。 (たとえば Python 用 GTasklet フレームワーク や、 peak.events フレームワーク において既に似たようなスケジューリング機能が実現されています。 しかし、現在のところジェネレータに値や例外を渡せないので、 すっきりしない小細工を弄しなければいけません):

    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=(), value=None, *exc):
            def resume():
                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)
    
  4. 単純な echo サーバーとそれをトランポリンを使って実行するコード (“nonblocking_read” と “nonblocking_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 patch #1223381 として入手可能です。

このパッチは2005年8月1日から2日に CVS にコミットされました。

謝辞

Raymond Hettinger (PEP 288) と Samuele Pedroni (PEP 325) が最初にジェネレータへ値や例外を送信したりジェネレータを「閉じる」アイディアを正式に提案してくれました。 Timothy Delaney はこの PEP のタイトルを考えてくれました。 Steven Bethard は以前のバージョンの編集を手伝ってくれました。 PEP 340 の謝辞もご覧下さい。

参考文献

TBD.

著作権

この文書はパブリックドメインです。