Python3 Advent Calendar 一日目 - Python とジェネレータ関数

みなさんジェネレータ関数使ってますか?

ジェネレータ関数便利ですよね!

無いと死んじゃいますよね!

というわけで Python3 Advent Calendar 一日目を担当する [twitter:@shomah4a] です。

ジェネレータ関数って?

とりあえずジェネレータ関数です。

めんどくさいのでソース

def calc_fibonacci():
    '''
    フィボナッチ数を生成し続けるジェネレータ関数だよ!
    '''

    yield 1
    yield 1

    n = 1
    m = 1

    while True:

        yield n + m
        o = n + m
        n = m
        m = o

こんな感じで関数の途中で yield を使って値を返す関数をジェネレータ関数と呼びます。

>>> calc_fibonacci
<function calc_fibonacci at 0x1ceb958>
>>> a = calc_fibonacci()
>>> a
<generator object calc_fibonacci at 0x1cef9b0>
>>> next(a) # 最初の yield の値
1
>>> next(a) # 二番目の yield の値
1
>>> next(a) # その次
2
>>> next(a) # さらに次
3

こいつを呼び出すとジェネレータオブジェクトが返ってきて、こいつに対してイテレートしてあげると、 yield で返した値をひとつずつ取り出せるというもの。
関数を抜けると同時に StopIteration 例外が投げられてイテレートが終了します。

next() を呼ぶごとに次の yield まで処理が進み、そこで一旦中断します。そのため、 yield による列挙処理とジェネレータをイテレートして逐次処理する部分を切り分けて考えることができます。

ジェネレータは長さが不定のデータや、一旦リストに落としこむとデータ量が大きくなってしまって大変なことになるデータを扱うときによく使います。
例えば Twitter のタイムラインを取得するといったものがその一例です。

先程作ったフィボナッチジェネレータはそもそも while True: で無限ループしているので終わりません。
なので、以下のようにして実行してしまった人は Ctrl+C なり Ctrl+Z なりでプロセスを止めないと止まりません。

>>> import time
>>> for f in calc_fibonacci():
...
...     print(f)
...     time.sleep(0.1)
...
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
.
.
.

ジェネレータ関数を抽象化したい?

そんなとっても便利なジェネレータ関数なのですが、以下のようにして実行しようとしてハマったことはありませんか?

ジェネレータ関数から別の関数に制御を渡して、そっちからまた yield した値を返したいなーと思って以下のように書きます。

def generatorA():

    yield 1
    yield 10
    yield 20

    generatorB(3)

    yield 30


def generatorB(n):

    for i in range(n):

        yield i

for i in generatorA():

    print(i)


要はこんな結果を期待しています。

>>> for i in generatorA():
...     print(i)
... 
1
10
20
0
1
2
30

でも、これの結果は

>>> for i in generatorA():
...     print(i)
... 
1
10
20
30

となって、 generatorB の中の yield の値は返りません。

勘のいい読者の方ならお気づきでしょうが generatorB(3) という呼び出しはジェネレータを返すので、中の処理は実行されないのです。

これを思ったとおりに動くようにするには以下のように書く必要があります。

def generatorA():

    yield 1
    yield 10
    yield 20

    for i in generatorB(3):
        yield i

    yield 30


def generatorB(n):

    for i in range(n):

        yield i

このようにすると

>>> for i in generatorA():
...     print(i)
... 
1
10
20
0
1
2
30

となって、思ったとおりに動きます。

ジェネレータの委譲

先程の generatorA, generatorB でやりたいことは、ジェネレータ関数から別のジェネレータ関数を呼び出して、そちらのジェネレータに処理を委譲したいということに尽きます。
その処理に関して generatorA での for による generatorB のイテレートは若干無駄です。

そこで登場するのが PEP 380 -- Syntax for Delegating to a Subgenerator | Python.org です。

これを使うと generatorA は以下のようにスッキリかけます。

def generatorA():

    yield 1
    yield 10
    yield 20

    yield from generatorB(3)

    yield 30

この移譲に使う yield は、委譲先の関数の返り値を返すようなので、

def generatorA():

    yield 1
    yield 10
    yield 20

    result = yield from generatorB(3)

    yield 30

というように返り値を受け取ることができます。多分。
この場合は generatorB が値を返さないので恐らく None ですが。

とここまでジェネレータ関数の委譲について説明しました。
でも実はこの機能、まだ実装されていません。

PEP380 によると Python 3.3 で実装予定らしいので、それまでお待ちください。
ちなみに 3.3 のリリース予定は PEP 398 -- Python 3.3 Release Schedule | Python.org によると来年の8月末だそうですので、半年以上先の話ですね。

Dev Guide を見つつ ソースコードリポジトリからソースを取ってきてビルドしても Syntax Error で落ちたので多分未実装。

委譲! じゃなくて以上!

次の人

[twitter:@jbking] さんに振ろうかな。
おねがいしまーす。