Python3 Advent Calendar 二十日目 PEP 380 -- Syntax for Delegating to a Subgenerator
Python3 Advent Calendarとやらに参加しろ、なんか書けと言われて泣く泣くキーボードを叩くatsuoishimotoです。
書けと言われてもネタがないのでどっかからパクろうと Python3 Advent Calendar を最初から目を通してみると、しょっぱなから良いネタがあった。何と初日の @shomah4aさんの エントリ。これをパクってしまおう。私のように清く正しい振る舞いを日頃から心がけてさえいれば、このようにあっさりとパクリの元ネタを見つけることが出来るのです。
元ネタの 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)