Pythonでのジェネレーターオブジェクトのリセット


153

複数のyieldで返されたジェネレーターオブジェクトがあります。このジェネレータを呼び出す準備は、かなり時間がかかる操作です。そのため、ジェネレータを何度か再利用したいと考えています。

y = FunctionWithYield()
for x in y: print(x)
#here must be something to reset 'y'
for x in y: print(x)

もちろん、私はコンテンツを単純なリストにコピーすることを念頭に置いています。ジェネレーターをリセットする方法はありますか?

回答:


119

別のオプションは、itertools.tee()関数を使用してジェネレーターの2番目のバージョンを作成することです。

y = FunctionWithYield()
y, y_backup = tee(y)
for x in y:
    print(x)
for x in y_backup:
    print(x)

これは、元のイテレーションがすべてのアイテムを処理しない可能性がある場合、メモリ使用の観点から有益です。


33
この場合に何が行われるのか疑問に思っているのであれば、それは本質的にリストの要素をキャッシュしていることになります。したがってy = list(y)、残りのコードを変更せずに使用することもできます。
ilya n。

5
tee()はデータを格納するリストを内部で作成するため、これは私の回答で行ったのと同じです。
nosklo 2009

6
implmentation(見docs.python.org/library/itertools.html#itertools.teeこの用途の遅延ロード戦略なので、オンデマンドでのみコピーされたリストに項目- )
Dewfy

11
@Dewfy:とにかくすべてのアイテムをコピーする必要があるため、どちらが遅くなります。
nosklo 2009

8
はい、この場合はlist()の方が適しています。tシャツは、リスト全体を消費していない場合にのみ役立ちます
重力

148

発電機は巻き戻すことはできません。次のオプションがあります。

  1. ジェネレーター関数を再度実行して、生成を再開します。

    y = FunctionWithYield()
    for x in y: print(x)
    y = FunctionWithYield()
    for x in y: print(x)
  2. ジェネレーターの結果をメモリまたはディスク上のデータ構造に保存します。これを繰り返し処理できます。

    y = list(FunctionWithYield())
    for x in y: print(x)
    # can iterate again:
    for x in y: print(x)

オプション1の欠点は、値を再度計算することです。CPUを集中的に使用する場合は、2回計算することになります。一方、2の欠点はストレージです。値のリスト全体がメモリに保存されます。値が多すぎると、実用的でない場合があります。

つまり、従来のメモリと処理のトレードオフがあります。値を保存したり、計算し直したりせずにジェネレータを巻き戻す方法は想像できません。


関数呼び出しの署名を保存する方法が存在するのでしょうか?FunctionWithYield、param1、param2 ...
Dewfy 2009

3
@Dewfy:確かに:def call_my_func():return FunctionWithYield(param1、param2)
nosklo

@Dewfy「関数呼び出しの署名を保存」とはどういう意味ですか?説明していただけますか?ジェネレーターに渡されたパラメーターを保存するということですか?
АндрейБеньковский

2
(1)のもう1つの欠点は、FunctionWithYield()はコストがかかるだけでなく、たとえばstdinから読み取っている場合など、再計算ができないことです。
最大

2
@Maxが言ったことをエコーするには、関数の出力が呼び出し間で変化する可能性がある(または変化する)場合、(1)予期しない結果や望ましくない結果が生じる可能性があります。
Sam_Butler

36
>>> def gen():
...     def init():
...         return 0
...     i = init()
...     while True:
...         val = (yield i)
...         if val=='restart':
...             i = init()
...         else:
...             i += 1

>>> g = gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
>>> g.send('restart')
0
>>> g.next()
1
>>> g.next()
2

29

おそらく最も簡単な解決策は、高価な部分をオブジェクトにラップしてジェネレータに渡すことです:

data = ExpensiveSetup()
for x in FunctionWithYield(data): pass
for x in FunctionWithYield(data): pass

このようにして、高価な計算をキャッシュできます。

すべての結果を同時にRAMに保持できる場合は、を使用list()してジェネレーターの結果をプレーンリストに具体化し、それを処理します。


23

古い問題に別の解決策を提供したい

class IterableAdapter:
    def __init__(self, iterator_factory):
        self.iterator_factory = iterator_factory

    def __iter__(self):
        return self.iterator_factory()

squares = IterableAdapter(lambda: (x * x for x in range(5)))

for x in squares: print(x)
for x in squares: print(x)

のようなものと比較した場合のこの利点list(iterator)は、これがO(1)スペースの複雑さであり、であるということlist(iterator)ですO(n)。欠点は、イテレータにアクセスでき、イテレータを生成した関数にはアクセスできない場合、このメソッドを使用できないことです。たとえば、次のことを行うのは妥当と思われるかもしれませんが、機能しません。

g = (x * x for x in range(5))

squares = IterableAdapter(lambda: g)

for x in squares: print(x)
for x in squares: print(x)

@Dewfy最初のスニペットでは、ジェネレーターは「squares = ...」の行にあります。ジェネレーター式は、yieldを使用する関数を呼び出すのと同じように動作しますが、このような短い例では、yieldを使用して関数を作成するよりも簡潔であるため、私は1つだけを使用しました。2番目のスニペットでは、generator_factoryとしてFunctionWithYieldを使用したので、iterが呼び出されるたびに呼び出されます。つまり、「for x in y」と書くたびに呼び出されます。
michaelsnowden 2016

良い解決策。これにより、実際には、ステートフルなイテレータオブジェクトではなく、ステートレスなイテラブルオブジェクトが作成されるため、オブジェクト自体が再利用可能になります。反復可能なオブジェクトを関数に渡し、その関数がオブジェクトを複数回使用する場合に特に役立ちます。
Cosyn 2018

5

GrzegorzOledzkiの答えでは不十分な場合は、おそらくsend()目標を達成するために使用できます。拡張ジェネレーターとyield式の詳細については、PEP-0342を参照してください。

更新:も参照してくださいitertools.tee()。これは、上記のトレードオフそのメモリ対処理の一部を含むが、それは可能性があるだけで発電した結果を格納比べていくつかのメモリを節約しますlist。それは、ジェネレーターの使用方法によって異なります。


5

出力が渡された引数とステップ番号のみに依存するという意味でジェネレーターが純粋であり、生成されたジェネレーターを再起動可能にしたい場合、便利なソートスニペットを次に示します。

import copy

def generator(i):
    yield from range(i)

g = generator(10)
print(list(g))
print(list(g))

class GeneratorRestartHandler(object):
    def __init__(self, gen_func, argv, kwargv):
        self.gen_func = gen_func
        self.argv = copy.copy(argv)
        self.kwargv = copy.copy(kwargv)
        self.local_copy = iter(self)

    def __iter__(self):
        return self.gen_func(*self.argv, **self.kwargv)

    def __next__(self):
        return next(self.local_copy)

def restartable(g_func: callable) -> callable:
    def tmp(*argv, **kwargv):
        return GeneratorRestartHandler(g_func, argv, kwargv)

    return tmp

@restartable
def generator2(i):
    yield from range(i)

g = generator2(10)
print(next(g))
print(list(g))
print(list(g))
print(next(g))

出力:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
0
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1

3

Tシャツの公式ドキュメントから:

一般に、あるイテレーターが別のイテレーターが開始する前にほとんどまたはすべてのデータを使用する場合は、tee()の代わりにlist()を使用する方が高速です。

したがってlist(iterable)、あなたのケースでは代わりに使用するのが最善です。


6
無限ジェネレーターはどうですか?
Dewfy 2016

1
考慮事項は速度だけではありません。list()
イテラブル

@Chris_Randsつまりtee()、1つのイテレータがすべての値を消費する場合-それが機能する方法teeです。
AChampion 2017

2
@Dewfy:(。貴重なデータを返すExpensiveSetup機能)無限発電機、使用アーロンDigullaの解決のために
ジェフLearman

3

ラッパー関数を使用して処理する StopIteration

ジェネレーターが使い果たされたときに追跡するジェネレーター生成関数に簡単なラッパー関数を書くことができます。これはStopIteration、反復の終わりに達したときにジェネレーターがスローする例外を使用して行われます。

import types

def generator_wrapper(function=None, **kwargs):
    assert function is not None, "Please supply a function"
    def inner_func(function=function, **kwargs):
        generator = function(**kwargs)
        assert isinstance(generator, types.GeneratorType), "Invalid function"
        try:
            yield next(generator)
        except StopIteration:
            generator = function(**kwargs)
            yield next(generator)
    return inner_func

上記のように、ラッパー関数がStopIteration例外をキャッチすると、(関数呼び出しの別のインスタンスを使用して)ジェネレーターオブジェクトを再初期化するだけです。

次に、以下のようにジェネレーター提供関数をどこかに定義すると、Python関数デコレーター構文を使用して暗黙的にラップできます。

@generator_wrapper
def generator_generating_function(**kwargs):
    for item in ["a value", "another value"]
        yield item

2

ジェネレータを返す関数を定義できます

def f():
  def FunctionWithYield(generator_args):
    code here...

  return FunctionWithYield

これで、何回でも好きなだけ行うことができます。

for x in f()(generator_args): print(x)
for x in f()(generator_args): print(x)

1
回答ありがとうございます。ただし、質問の主なポイントは、作成を避けることでした。内部関数を呼び出すと、作成が非表示になるだけです。作成は2回です
Dewfy

1

あなたが高価な準備で何を意味していたのかはわかりませんが、あなたは実際に持っていると思います

data = ... # Expensive computation
y = FunctionWithYield(data)
for x in y: print(x)
#here must be something to reset 'y'
# this is expensive - data = ... # Expensive computation
# y = FunctionWithYield(data)
for x in y: print(x)

その場合は、再利用してみませんdataか?


1

イテレータをリセットするオプションはありません。イテレータは通常、next()関数を反復処理するときにポップアウトします。唯一の方法は、反復子オブジェクトで反復する前にバックアップを取ることです。以下を確認してください。

アイテム0〜9のイテレーターオブジェクトの作成

i=iter(range(10))

ポップアウトするnext()関数を反復する

print(next(i))

イテレータオブジェクトをリストに変換する

L=list(i)
print(L)
output: [1, 2, 3, 4, 5, 6, 7, 8, 9]

したがって、アイテム0はすでにポップアウトされています。また、イテレータをリストに変換すると、すべてのアイテムがポップされます。

next(L) 

Traceback (most recent call last):
  File "<pyshell#129>", line 1, in <module>
    next(L)
StopIteration

したがって、反復を開始する前に、イテレータをバックアップ用のリストに変換する必要があります。リストはイテレータに変換できますiter(<list-object>)


1

これで、使用することができますmore_itertools.seekableリセットイテレータを可能にします(サードパーティ製のツールを)。

経由でインストール > pip install more_itertools

import more_itertools as mit


y = mit.seekable(FunctionWithYield())
for x in y:
    print(x)

y.seek(0)                                              # reset iterator
for x in y:
    print(x)

注:イテレータを進めるとメモリの消費量が増えるので、大きなイテラブルに注意してください。


1

itertools.cycle() を使用してそれを行うには、このメソッドでイテレーターを作成し、そのイテレーターでforループを実行して、その値をループします。

例えば:

def generator():
for j in cycle([i for i in range(5)]):
    yield j

gen = generator()
for i in range(20):
    print(next(gen))

0から4までの20個の数値が繰り返し生成されます。

ドキュメントからのメモ:

Note, this member of the toolkit may require significant auxiliary storage (depending on the length of the iterable).

+1が機能するが、2つの問題が発生する1)ドキュメントに「コピーを作成する」と記載されているため、メモリのフットプリントが大きい2)無限ループは間違いなく私が望んでいない
Dewfy

0

わかりました、ジェネレーターを複数回呼び出したいと言いますが、初期化は高価です...このようなものはどうですか?

class InitializedFunctionWithYield(object):
    def __init__(self):
        # do expensive initialization
        self.start = 5

    def __call__(self, *args, **kwargs):
        # do cheap iteration
        for i in xrange(5):
            yield self.start + i

y = InitializedFunctionWithYield()

for x in y():
    print x

for x in y():
    print x

または、イテレータプロトコルに従って独自のクラスを作成し、何らかの「リセット」関数を定義することもできます。

class MyIterator(object):
    def __init__(self):
        self.reset()

    def reset(self):
        self.i = 5

    def __iter__(self):
        return self

    def next(self):
        i = self.i
        if i > 0:
            self.i -= 1
            return i
        else:
            raise StopIteration()

my_iterator = MyIterator()

for x in my_iterator:
    print x

print 'resetting...'
my_iterator.reset()

for x in my_iterator:
    print x

https://docs.python.org/2/library/stdtypes.html#iterator-types http://anandology.com/python-practice-book/iterators.html


問題をラッパーに委任するだけです。コストのかかる初期化によってジェネレータが作成されると想定します。私の質問は、あなたの中でリセットする方法についてでした__call__
Dewfy

コメントへの応答として2番目の例を追加しました。これは基本的に、resetメソッドを備えたカスタムジェネレーターです。
tvt173 2016年

0

私の答えは少し異なる問題を解決します:ジェネレーターの初期化にコストがかかり、生成された各オブジェクトの生成にコストがかかる場合。しかし、複数の関数でジェネレータを複数回使用する必要があります。ジェネレーターと生成された各オブジェクトを1回だけ呼び出すために、スレッドを使用し、さまざまなスレッドで各消費メソッドを実行できます。GILにより、真の並列処理を実現できない可能性がありますが、目標は達成できます。

このアプローチは、ディープラーニングモデルが多くの画像を処理する次の場合にうまく機能しました。その結果、画像上の多くのオブジェクトに対して多くのマスクが作成されます。各マスクはメモリを消費します。さまざまな統計やメトリックを作成する約10のメソッドがありますが、それらはすべての画像を一度に取得します。すべての画像がメモリに収まりません。moethodsは、イテレータを受け入れるように簡単に書き換えることができます。

class GeneratorSplitter:
'''
Split a generator object into multiple generators which will be sincronised. Each call to each of the sub generators will cause only one call in the input generator. This way multiple methods on threads can iterate the input generator , and the generator will cycled only once.
'''

def __init__(self, gen):
    self.gen = gen
    self.consumers: List[GeneratorSplitter.InnerGen] = []
    self.thread: threading.Thread = None
    self.value = None
    self.finished = False
    self.exception = None

def GetConsumer(self):
    # Returns a generator object. 
    cons = self.InnerGen(self)
    self.consumers.append(cons)
    return cons

def _Work(self):
    try:
        for d in self.gen:
            for cons in self.consumers:
                cons.consumed.wait()
                cons.consumed.clear()

            self.value = d

            for cons in self.consumers:
                cons.readyToRead.set()

        for cons in self.consumers:
            cons.consumed.wait()

        self.finished = True

        for cons in self.consumers:
            cons.readyToRead.set()
    except Exception as ex:
        self.exception = ex
        for cons in self.consumers:
            cons.readyToRead.set()

def Start(self):
    self.thread = threading.Thread(target=self._Work)
    self.thread.start()

class InnerGen:
    def __init__(self, parent: "GeneratorSplitter"):
        self.parent: "GeneratorSplitter" = parent
        self.readyToRead: threading.Event = threading.Event()
        self.consumed: threading.Event = threading.Event()
        self.consumed.set()

    def __iter__(self):
        return self

    def __next__(self):
        self.readyToRead.wait()
        self.readyToRead.clear()
        if self.parent.finished:
            raise StopIteration()
        if self.parent.exception:
            raise self.parent.exception
        val = self.parent.value
        self.consumed.set()
        return val

使用法:

genSplitter = GeneratorSplitter(expensiveGenerator)

metrics={}
executor = ThreadPoolExecutor(max_workers=3)
f1 = executor.submit(mean,genSplitter.GetConsumer())
f2 = executor.submit(max,genSplitter.GetConsumer())
f3 = executor.submit(someFancyMetric,genSplitter.GetConsumer())
genSplitter.Start()

metrics.update(f1.result())
metrics.update(f2.result())
metrics.update(f3.result())

あなたは単にitertools.isliceasyncのために再発明するかaiostream.stream.take、そしてこの投稿はasyn / awaitの方法でそれを可能にしますstackoverflow.com/a/42379188/149818
Dewfy

-3

これは、コードオブジェクトによって実行できます。これがその例です。

code_str="y=(a for a in [1,2,3,4])"
code1=compile(code_str,'<string>','single')
exec(code1)
for i in y: print i

1 2 3 4

for i in y: print i


exec(code1)
for i in y: print i

1 2 3 4


4
まあ、初期化コードの2度の実行を避けるために、実際にジェネレータをリセットする必要がありました。あなたのアプローチは(1)とにかく2回初期化を実行します、(2)それはexecそのような単純なケースではわずかに推奨されないことを含みます。
Dewfy 2013
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.