イテレータそのものはbreak
等で最後まで回らない可能性があるけど、ジェネレータ内部のイテレーションは最後まで回したいという場合がまれにある。
うまくいかない例
例として次のrange_printsum
はrange
関数にその範囲の総和をprintする処理を追加したものだ。
def range_printsum(*args, **kwargs):
sum_ = 0
for i in range(*args, **kwargs):
sum_ += i
yield i
print(sum_)
このジェネレータ実装ではイテレーションを途中で止めてしまうと、sum_
に格納されるのは実際にyield
した数の合計だし、そもそもsum_
は出力されない。
>>> from gen import range_printsum
>>> for i in range_printsum(11):
... pass
...
55
>>> for i in range_printsum(11):
... if i > 5:
... break
...
>>>
うまくいく例
そこでGeneratorExit
を捕捉する。これはジェネレータが閉じられた(ジェネレータのclose()
が呼ばれた)時に送出される。これはfor
等のループが中断された場合でも捕捉できる。
GeneratorExit
を捕捉した後は値を返さず、加算だけ行うようrange_printsum
を修正する。
def range_printsum(*args, **kwargs):
sum_ = 0
stop_yield = False
for i in range(*args, **kwargs):
sum_ += i
if not stop_yield:
try:
yield i
except GeneratorExit:
stop_yield = True
print(sum_)
今度は期待通りに動く。
>>> from gen import range_printsum
>>> for i in range_printsum(11):
... pass
...
55
>>> for i in range_printsum(11):
... if i > 5:
... break
...
55
>>>
このように中断しても動く。
>>> try:
... for i in range_printsum(11):
... if i > 5:
... raise IndexError
... except IndexError:
... pass
...
55
>>>
ジェネレータの仕様による注意点として、一度閉じられたジェネレータは値を生成してはならない。GeneratorExit
を捕捉した後に何らかの値をyield
するとRuntimeError: generator ignored GeneratorExit
というエラーが発生する。