みねこあ

mineko. A! ―from mi-neko online.

2007-04-10

[]ジェネレータ と コルーチン 13:00 ジェネレータ と コルーチンを含むブックマーク

ジェネレータ

ジェネレータ(Generator)とは、イテレータコンパチなインターフェイスを持つけど、 指すべきコレクションがあるわけでもなく、そのたんびに 値を作り出して返すようなモノを作るモノをいいます。

Python では、yield 句を含む 関数は ジェネレータとなります。

def myGenerator:
    yield 1
    yield 2
    yield 3

gen = myGenerator()

for i in gen:
    print i

上記コード上の gen はイテレータと同じインターフェイスを持っていて、 next() を呼び出せば次のyield を返し gen の最後まで実行すると StopIteration 例外を発生させます。

つまり for 文を使わない場合は以下のようになります。

print gen.next()   # "1"
print gen.next()   # "2"
print gen.next()   # "3"
print gen.next()   # ここで StopIteration 例外が発生

但し、ジェネレータはループで回す事を前提としてインターフェイスが整えられています。 一番最初のコードも、説明のため「値を次々と返すモノ」を一度変数に束縛していますが、 たいていはこんな風に、

def myGenerator:
    yield 1
    yield 2
    yield 3

for i in myGenerator():
    print i

直接 イテレータを要求するところで呼び出します。 と、ここまで書けば Pythoner がいつもお世話になっている range()関数をよく似ている、と気が付くと思います。*1

ジェネレータ表現

前回リスト内包表現で書いた Sumを求める処理

sum( [ ord(i) for i in "hogehogehoge" ] ) & 0xFF

は、リスト内包表現で示される新たなリストを一度生成してから、 そのリストに対して sum() を行いますが、これはメモリとCPU の無駄遣いだ、というわけで、 リストではなくジェネレータを生成する ジェネレータ表現 が Python 2.4 から導入されました。ジェネレータ表現で上記コードを書き直すと、

sum( ord(i) for i in "hogehogehoge" ) & 0xFF

となります。実は 丸カッコの中に リスト内包表現のような記述があると、 無名のジェネレータを作りそれをコールしたようになります。

ジェネレータ表現はなにも関数の引数リストの中身だけに限定されるわけではありません。 丸カッコで囲まれていればよいのです。 なので、以下のように生成された ジェネレータを変数に束縛することもできます。 (ファーストクラスのオブジェクトだということです)

>>> gen = (i for i in range(0,16))
>>> gen.next()
0
>>> gen.next()
1
>>> gen.next()
2
>>> gen.next()
3
      ・
      ・
      ・
>>> gen.next()
15
>>> gen.next()
Traceback (most recent call last):
  File "<pyshell#19>", line 1, in -toplevel-
    gen.next()
StopIteration

つまり、 gen = (i for i in range(0,16)) は

def myGenerator():
    for i in range(0,16):
       yeild i

gen = myGenerator()

と同義です。 ジェネレータ表現は 無名のジェネレータ、またはジェネレータのリテラル表記と見ることが出来ます。

コルーチン

ジェネレータが作る「値を次々に返すモノ」は、味付けこそイテレータ風ですが、 本質的には コルーチン(coroutine) です。 コルーチンとは、「実行の途中でリターンでき、次回コール時にはそこから処理を再開することが出来るモノ」で*2、 「メインとサブ」という関係を持たないルーチンを示すします。

Python のジェネレータが作るモノは、コール出来ない(.__call__() ではなく、.next() を呼ぶ)ので、 直感的に コルーチン めいて見えません。

なぜ、Pythonが直接コルーチンを定義するではなく、それのファクトリを定義させるかというと、 そうするとコルーチンが引数を持ったときの挙動が 直感的でなくなるから・・というのがあると思います。

403 Forbiddenさんの例そのままの借用ですが、もし、Pythonが直接 「値を次々に返すモノ」を定義出来た場合、 以下のコードはどう動くかは、直感的にはわかりません。

'''
Python として有効なコードではない。
'''
def myCoroutine( initval ):
    yield initval
    yield initval + 1
    yield initval + 2

print myCoroutine( 5 )          # "5"
print myCoroutine( 3 )          # "6"? それとも "4"?
print myCoroutine( 10 )         # "7"? それとも "12"?

Python の ジェネレータで作られるコルーチンは(.__call__(...) ではなく .next() で呼ばれるので) 引数が無いモノでは無くてはならないという制約があります。おかげでこの混乱から無縁です。 ジェネレータに引数を渡すと、それはコルーチンの生成時引数でしかありません。

Python のジェネレータは コルーチンファクトリですが、ジェネレータをカリー化して引数をゼロにしたものが コルーチン とも言えるかも(猫の関数言語の知識は「なんちゃって」なので明後日の方向なことを言ってるかも・・)

以上、なんだかモヤッていた Python の ジェネレータについて、自分向けに早分かりをまとめてみた次第です。

余談

Python のジェネレータ表現は、書いているときは直感的で気持ちよいのですが、少したってから見直すと結構読みづらく感じます。リスト内包表現のときはそうは感じませんでした。

なんでかな〜、と考えたのですが、リスト内包表現もジェネレータ表現も 他のPython のシンタックスとはちょっと異質で、頭の中で文法をスイッチして読んでいるようなのですが、リスト内包表現の場合は 角カッコ内の for とか if が悪目立ちしてるので(笑)、それをトリガに脳スイッチが切り替わるのですが、ジェネレータ表現は一見自然に馴染んでしまうので 読みづらく感じるようです。

これは慣れかしら。

*1:でも range関数は ジェネレータではなく、本当にその範囲の値を全て持つリストをつくって返しやがります。 だから例えば for i in range(0,999999999): なんて出来ません。(MemoryError 例外が発生します)こういうときは xrange関数を使います。 xrangeはジェネレータです(たぶん)

*2:多分。間違ってるかも

トラックバック - http://d.hatena.ne.jp/minekoa/20070410/1176177653