[Python入門]ジェネレータの高度な話題:Python入門(2/2 ページ)
ジェネレータイテレータのthrow/closeメソッド、ジェネレータを簡潔に記述できるジェネレータ式、他のイテレータに処理を委譲するyield from式を取り上げる。
yield from式
前回紹介したyield式には、もう一つの形式がある。それがyield from式だ。これは「yield from 式」のような形で記述する。このとき「式」を評価した結果は反復可能オブジェクトまたはイテレータである必要がある。それが反復可能オブジェクトであれば、yield from式ではそれからイテレータを取り出して、そのイテレータから値を呼び出し側に順次返送できる機能だ。yield from式で使用するイテレータのことを「サブジェネレータ」と、yield from式で他のイテレータを利用することを「サブジェネレータへの委譲」呼ぶこともある。
簡単な例を以下に示す。
def sample_geniter():
yield from [1, 2, 3, 4]
print('finished')
for num in sample_geniter():
print(num)
print('hello')
ここではsample_geniterジェネレータ関数は、[0, 1, 2, 3]というリストに「処理を委譲」する。リストは反復可能オブジェクトなので、そのイテレータを使って、yield from式が実行されるたびにその要素を1つずつ返送してくれるということだ。
実行結果を以下に示す。
ここで重要なのは、yield from式は指定したサブジェネレータが持つ全ての値が返送されるまでは、構文上は処理が完了しないという点だ。そのため、上のコードでは全ての要素が列挙された後に、次の行が実行されて「finished」と表示され、yield式に到達することなくジェネレータイテレータが終了する。すると、StopIteration例外が発生するので、そこでfor文が終了する。
上のコードをfor文を使わずにwhile文を使って書き直したものを以下に示す。これを実行してみると、「print('finished')」行が実行された後にStopIteration例外が発生しているのが分かるはずだ(実行結果は省略)。
def sample_geniter():
yield from [0, 1, 2, 3]
print('finished')
mygeniter = sample_geniter()
num = next(mygeniter)
while True:
print(num)
num = next(mygeniter)
次に、自作のジェネレータイテレータを使って、yield from式についてもう少し見ていこう。ここでは次のようなジェネレータ関数を2つ定義する。
def sub_generator():
value = 0
while True:
if value == 'stop':
return -1
else:
result = sum(range(value+1))
value = yield result
def outer_geniter():
value = yield from sub_generator()
yield value
sub_generatorジェネレータ関数は、outer_geniterジェネレータ関数のyield from式で使われる。outer_geniterジェネレータ関数はsub_generatorジェネレータ関数に処理を委譲して、値の列挙が終わったところで「そのyield from式の値」をyieldして処理を終了している。
だが、このコードには幾つか不明な点がある。
- sub_generatorジェネレータ関数には、yield式ではなくreturn文で値を返しているところがある(ジェネレータ関数では呼び出し側に値を返すにはyield式を使用して、return文で値を返すとStopIteration例外が発生する)
- outer_geniterジェネレータ関数でyield from式の値がどこから得られるのか分からない
- sub_generatorジェネレータ関数ではyield式の値を使用しているが、それはどこから得られるのか
最初の2つに関しては、「サブジェネレータではreturn文で値を返してもよい」「その値が、それに処理を委譲した側のyield from式の値となる」と説明できる(ただし、サブジェネレータとして書いたものを通常のジェネレータイテレータとして使用すると、return文が実行された時点でStopIteration例外が発生する。Pythonの処理系がこの辺をうまく処理してくれているということだ)。
つまり、上のコードではsub_generatorジェネレータ関数(が生成するジェネレータイテレータ)で変数valueの値が'stop'であれば、return -1がouter_generatorジェネレータ関数へと返送されて、それがyield from式の値となるということだ。
では、変数valueの値が'stop'になるのはどういうときだろう。実は、外側の(サブジェネレータを使用する側の)ジェネレータイテレータに対して、__next__/send/throw/closeメソッドを実行すると、そのまま委譲されたジェネレータの対応するメソッドが実行されるようになっているのだ。
実際のコード例を見てみよう。
mygeniter = outer_geniter()
print(next(mygeniter)) # これによりサブジェネレータのコードが実行開始
これを実行すると次のようになる。
next関数にジェネレータイテレータを渡すと、最終的にサブジェネレータ(sub_generatorジェネレータ関数から作られたジェネレータイテレータ)の__next__メソッドが呼び出され、そのコードが実行される。
そこでは、range関数を使ってrange(0, value+1)の範囲に含まれる整数値の総和を取得している。変数valueの初期値は0であり、sum(range(0, 1))は0なので(最終値は範囲に含まれないことに注意)数値「0」が委譲した側を経由して呼び出し側に返されて、これが表示された。
次にsendメソッドを使ってみよう。
print(mygeniter.send(5)) # sum(range(0, 6))を計算(0〜5の総和)
print(mygeniter.send('stop')) # 処理を終了
今度はsendメソッドで「5」と文字列の「stop」を送信している。nextメソッドと同様、これによりサブジェネレータのsendメソッドが呼び出されて、それらの値がサブジェネレータに渡される。
「mygeniter.send(5)」として「5」を渡すとsum(range(0, 6))が計算されて、数値「15」が返される。「mygeniter.send('stop')」として文字列「stop」を渡すと今度は変数valueの値が'stop'なので、yield式ではなくreturn文によって「-1」が返送されて、それがyield from式の値となる。処理を委譲した側のジェネレータイテレータでは、それをyield式で返送している。
よって、実行結果は次のようになる。
この後はnext関数にジェネレータイテレータを渡しても、StopIteration例外が発生する。
また、ここでは取り上げなかったthrowメソッドやcloseメソッドについても同様に、委譲する側のジェネレータイテレータに対してそれらを呼び出すことで、サブジェネレータにそれらのメソッドが転送される。
この例では分かりにくいが、yield from式による「サブジェネレータへの処理の委譲」は大規模なジェネレータ関数の処理を複数の関数に分割するためのものだ。本稿ではそうした例を説明するには分量が足りないが、取りあえずはそうした機構が用意されているということを覚えておけばよいだろう。
まとめ
今回はジェネレータイテレータのthrow/closeメソッド、ジェネレータ式、yield from式について見た。次回はデコレータを紹介する。
今回のまとめ
- ジェネレータイテレータには、指定した例外を発生させるためのthrowメソッド、GeneratorExit例外を発生させるcloseメソッドがある
- throwメソッドでは例外を処理した後に、実行を続けて、次の値を返送する
- closeメソッドは、ジェネレータイテレータが処理を完了する前に削除される際に自動的に呼び出されるので、そこで内部で使用しているリソース解放などの後始末を行う
- ジェネレータ式を使うと、ジェネレータイテレータをより簡潔に記述できる
- yield from式を使うと、他のイテレータ(サブジェネレータ)に処理を委譲できる
Copyright© Digital Advantage Corp. All Rights Reserved.