Pythonでコルーチン

Luaはコルーチンがとても特徴的で、こんなに継続オブジェクトが扱いやすい言語は、他にありません。
http://www.lua.org/manual/5.1/manual.html#2.11
Luaまではムリでも、Pythonでそれに近いことができないか、挑戦してみました。

Python2.5以上じゃないとダメですが、ジェネレータ関数を入力として作成される、コルーチン「もどき」オブジェクトです。

class Coroutine:
    def __init__(self, func):
        self._gen = func
        self._itr = None
        self.response = None
        self.alive = True

    def resume(self, *args):
        if not self.alive:
            raise "cannot resume dead coroutine"
        try:
            if self._itr is None:
                self._itr = self._gen(*args)
                self.response = self._itr.next()
            else:
                self.response = self._itr.send(args)
            return self.response
        except StopIteration:
            self.alive = False
            self.response = None

って、これだけじゃ意味不明なので、実例を。

# coroutine
def zig_zag(msg):
    for i in xrange(4):
        print "(%s to co)" % msg,
        print "zig",
        msg = yield i
        
        print "(%s to co)" % msg,
        print "zag",
        msg = yield i

# user
co = Coroutine(zig_zag)
i = 0
while(co.alive):
    print "(%s from co)" % co.resume(i)
    i += 1

出力はこんな感じ。

(0 to co) zig (0 from co)
(1 to co) zag (0 from co)
(2 to co) zig (1 from co)
(3 to co) zag (1 from co)
(4 to co) zig (2 from co)
(5 to co) zag (2 from co)
(6 to co) zig (3 from co)
(7 to co) zag (3 from co)
(None from co)

そうです。ジェネレータから作られるイテレータをそのまま使うのではなく、コルーチンとして翻訳しているだけです。

だいぶLuaっぽくなりました。

関数のようにパラメータを転送するには、404 Not Foundで追加された機能が必要なので、Pythonのバージョンは2.5以上になってしまいますが、コルーチンへのパラメータ入力が必要なければ、2.4以下でも似たようなことは可能かもしれません。

ただこれ、コルーチンをネストしたいときが問題です。Luaだとyieldが関数なので、関数のネストとして単純にリファクタリングできます。が、Pythonのyieldは、その関数内でのみ有効な「構文」です。

コルーチンネストはこんな感じ。

def main_flow():
    print "main start"
    yield
    
    # This way is lessor than Lua
    co = Coroutine(sub_flow)
    while(co.alive): yield co.resume()
    
    print "main end"

def sub_flow():
    print "sub start"
    yield
    for i in xrange(10):
        print "sub %d" % i
        yield
    print "sub end"

# execute step by step with interval
import time
co = Coroutine(main_flow)
while(co.alive):
    co.resume()
    time.sleep(0.5)

main_flowがsub_flowを使うとき、単なる関数呼び出しではなく、「サブコルーチンを作成して転送」する必要がありました。まあ、Luaには及ばないけど、そこまでひどいわけじゃないですね。コルーチンの刻み幅って、そんなに深くする必要はないだろうし。