読者です 読者をやめる 読者になる 読者になる

Python3 Advent Calendar 二十日目 PEP 380 -- Syntax for Delegating to a Subgenerator

python

Python3 Advent Calendarとやらに参加しろ、なんか書けと言われて泣く泣くキーボードを叩くatsuoishimotoです。

書けと言われてもネタがないのでどっかからパクろうと Python3 Advent Calendar を最初から目を通してみると、しょっぱなから良いネタがあった。何と初日の @さんの エントリ。これをパクってしまおう。私のように清く正しい振る舞いを日頃から心がけてさえいれば、このようにあっさりとパクリの元ネタを見つけることが出来るのです。
元ネタの Python とジェネレータ関数 では Python 3.3に組み込まれる予定の PEP 380 -- Syntax for Delegating to a Subgenerator が紹介されているが、これをもうちょっと詳しく説明してみよう。

イテレータの委譲

これまでの Python では、ジェネレータの中で他のジェネレータの値を返すとき、こんな感じで書く必要があった。

def gen():
    for v in subgenerator():
        yield v

これを、Python3.3 からは

def gen():
    yield from subgenerator()

と書ける。また yield from にはジェネレータだけではなく、普通のイテレート可能なオブジェクトも指定できる。

# Python 3.2までのジェネレータ
def gen_3.2():
    l = [1,2,3,4,5]
    for v in l:
        yield v
# Python 3.3までのジェネレータ
def gen_3.3():
    yield from [1,2,3,4,5]

ジェネレータの戻り値

yield from はループを省略するためだけに追加されたわけではない。こんなコードを見てみよう。

@defer.inlineCallbacks
def connectpop3(pop3, user, passwd):
    yield pop3.login(user, passwd)
    yield pop3.listSize()
    uids = yield pop3.listUID()
    for i in range(len(uids)):
        lines = []
       yield pop3.retrieve(i, lines.append)
       mail = b"\r\n".join(lines)
       do_mail(mail)

Twisted の inlineCallbacks を使って、POP3サーバからメールを取得する処理だ。一見、普通の関数に見えるかもしれないが、POP3サーバへのログイン等はジェネレータを使ってすべて非同期に実行されるようになっている。なっている、というか、Twisted はまだ Python3 をサポートしていないので、実はまだ「なってない」のだが。

このコードをちょっと整理して、メールの本文を取得する部分を別の関数に切り出してみるとしよう。

@defer.inlineCallbacks
def retrieveMail(pop3, ret):
    lines = []
    yield pop3.retrieve(i, lines.append)
    mail = b"\r\n".join(lines)
    ret.append(mail)

@defer.inlineCallbacks
def connectpop3(pop3, user, passwd):
    yield pop3.login(user, passwd)
    yield pop3.listSize()
    uids = yield pop3.listUID()
    for i in range(len(uids)):
       ret = []
       for y in retrieveMail(pop3, ret):
            yield y
       mail = ret[0]
       do_mail(mail)

現在のPythonではこんな感じで、取得したメール本文を呼び出し元に返すために、わざわざリストを渡してその中に設定して返す、などと面倒くさい手段が必要となってしまう。ジェネレータは値を returnできないし、yieldで戻す値は非同期通信処理の制御に使ってしまっているからだ。

そこでPEP380では、ジェネレータでも値を返せるように変更された。上のコードはこのようにすっきりと書けるようになったのである。

@defer.inlineCallbacks
def retrieveMail(pop3):
    lines = []
    yield pop3.retrieve(i, lines.append)
    return b"\r\n".join(lines)

@defer.inlineCallbacks
def connectpop3(pop3, user, passwd):
    yield pop3.login(user, passwd)
    yield pop3.listSize()
    uids = yield pop3.listUID()
    for i in range(len(uids)):
       ret = []
       mail = yield from retrieveMail(pop3, ret)
       do_mail(mail)

ジェネレータプロトコル

Python2.5からジェネレータオブジェクトにsend()などのメソッドが追加され、ジェネレータ外から情報を送り込めるようになった。

>>> def gen():
...     value = yield 1
...     value = yield value * 2
...     value = yield value * 3
...
>>> g = gen()
>>> g.next()
1
>>> g.send(10)
20
>>> g.send(20)
60

この程度の処理なら必要ないが、大きめな処理ではジェネレータを複数の小さなジェネレータに分割したくなる。しかし、そう簡単には分割できない。

def func1():
    value = yield 1 # valueをここで受け取りたいが
    return value

def gen():
    for v in func1():
        value = yield v # 実際にはここで受け取ってしまう

問題は、ジェネレータのsend()メソッドで値を送り込めるのは、最後のyieldだけ、という所だ。この例では、本来func1()内のyieldに値を送りたいのだが、gen()でしか受け取れないため、いったん受け取った値を手動で子ジェネレータにsend()しなければならない。分割できないわけではないが、なかなか面倒なスクリプトになる。

そこで yield from だ。値を個別にyieldするのはやめて、yield fromでまとめて委譲すれば、直接末端のジェネレータに値を送り込めるようになる。

def func1():
    value = yield 1
    return value

def func2(value):
    value = yield value * 2
    return value

def func3(value):
    value = yield value * 3
    return value

def gen():
    value = yield from func1()
    value = yield from func2(value)
    value = yield from func3(value)

というわけで

以上で Python3 Advent Calendar 二十日目を終わる。二十一日目は突然 @先生に捕獲されてしまって大変気の毒な @さんにお願いしよう。