Pythonのジェネレータは便利ですが、for文で一度ループに使用したあと、またfor文で使おうとするとカラッポのようになります。
def i(n):
yield n + 1
yield n + 2
g = i(10)
print('first time:')
for n in g:
print(n)
print('second time:')
for n in g:
print(n)
出力は
first time:
11
12
second time:
となります。2回目のfor
ループでは要素がひとつもイテレートされていません。
2回目以降のfor
ループでも11
と12
が帰ってきてほしい、というようなシチュエーションの場合、以下のようなテクニックを使うとうまくいきました。
class ReiteratableWrapper(object):
def __init__(self, f):
self._f = f
def __iter__(self):
return self._f()
def i(n):
yield n + 1
yield n + 2
import functools
f = functools.partial(i, 10)
g2 = ReiteratableWrapper(f)
for n in g2:
print(n)
for n in g2:
print(n)
解説
ReiteratableWrapper
クラスはジェネレータ関数をひとつ受け取ります。内部では引数を与えずに呼び出すので、引数をともなったジェネレータ呼び出しを行いたい場合は例のように functools.partial
関数などを使って引数をバインドした引数なしの関数を作成しておきます。
for文のinのあとのコンテキストで評価された式は __iter__
メソッドが呼ばれます。
そのため、ReiteratableWrapper
のインスタンスは毎回あらたにジェネレータを自動的に作ってくれるようになります。
ただし__iter__
メソッドが評価されるたびにジェネレータ関数を評価し、実行していますのでジェネレータ関数の実行回数に制限があるなどジェネレータ関数呼び出しに副作用がある場合は何回も繰り返し使える、とはならないことがあるかと思います。
ファイルから順番に読み込んでメモリを節約する、DBに毎回クエリを発行して順番にイテレーションする、など繰り返し行える内容には今回紹介したテクニックは有効と思います。