Generatorobjekt in Python zurücksetzen

153

Ich habe ein Generatorobjekt, das mit mehreren Erträgen zurückgegeben wird. Die Vorbereitung zum Aufrufen dieses Generators ist ziemlich zeitaufwändig. Deshalb möchte ich den Generator mehrmals wiederverwenden.

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

Natürlich denke ich daran, Inhalte in eine einfache Liste zu kopieren. Gibt es eine Möglichkeit, meinen Generator zurückzusetzen?

Dewfy
quelle

Antworten:

118

Eine andere Möglichkeit besteht darin, die itertools.tee()Funktion zu verwenden, um eine zweite Version Ihres Generators zu erstellen:

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

Dies kann aus Sicht der Speichernutzung von Vorteil sein, wenn die ursprüngliche Iteration möglicherweise nicht alle Elemente verarbeitet.

Ameisen Aasma
quelle
33
Wenn Sie sich fragen, was es in diesem Fall tun wird, werden im Wesentlichen Elemente in der Liste zwischengespeichert. Sie können es also genauso gut y = list(y)mit dem Rest Ihres Codes unverändert verwenden.
ilya n.
5
tee () erstellt intern eine Liste zum Speichern der Daten. Das ist also das gleiche wie in meiner Antwort.
Nosklo
6
Schauen Sie sich die Implementierung an ( docs.python.org/library/itertools.html#itertools.tee ) - dies verwendet eine Strategie des verzögerten Ladens , sodass Elemente nur bei Bedarf kopiert werden
Dewfy
11
@ Dewfy: Was langsamer sein wird, da alle Elemente sowieso kopiert werden müssen.
Nosklo
8
Ja, list () ist in diesem Fall besser. Tee ist nur nützlich, wenn Sie nicht die gesamte Liste verbrauchen
Gravitation
148

Generatoren können nicht zurückgespult werden. Sie haben folgende Möglichkeiten:

  1. Führen Sie die Generatorfunktion erneut aus und starten Sie die Generierung neu:

    y = FunctionWithYield()
    for x in y: print(x)
    y = FunctionWithYield()
    for x in y: print(x)
  2. Das Speichern des Generators führt zu einer Datenstruktur auf dem Speicher oder der Festplatte, die Sie erneut wiederholen können:

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

Der Nachteil von Option 1 ist, dass die Werte erneut berechnet werden. Wenn das CPU-intensiv ist, berechnen Sie am Ende zweimal. Auf der anderen Seite ist der Nachteil von 2 der Speicher. Die gesamte Werteliste wird gespeichert. Wenn es zu viele Werte gibt, kann dies unpraktisch sein.

Sie haben also den klassischen Kompromiss zwischen Speicher und Verarbeitung . Ich kann mir keine Möglichkeit vorstellen, den Generator zurückzuspulen, ohne die Werte zu speichern oder erneut zu berechnen.

nosklo
quelle
Möglicherweise gibt es eine Möglichkeit, die Signatur des Funktionsaufrufs zu speichern? FunctionWithYield, param1, param2 ...
Dewfy
3
@ Dewfy: sicher: def call_my_func (): return FunctionWithYield (param1, param2)
nosklo
@ Dewfy Was meinst du mit "Signatur des Funktionsaufrufs speichern"? Könnten Sie bitte erklären? Meinen Sie damit, die an den Generator übergebenen Parameter zu speichern?
Андрей Беньковский
2
Ein weiterer Nachteil von (1) ist auch, dass FunctionWithYield () nicht nur kostspielig sein kann, sondern auch nicht neu berechnet werden kann, z. B. wenn es von stdin liest.
Max
2
Um zu wiederholen, was @Max gesagt hat, kann (1) unerwartete und / oder unerwünschte Ergebnisse liefern, wenn sich die Ausgabe der Funktion zwischen Aufrufen ändern könnte (oder wird).
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
aaab
quelle
29

Die wahrscheinlich einfachste Lösung besteht darin, das teure Teil in ein Objekt zu wickeln und an den Generator weiterzugeben:

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

Auf diese Weise können Sie die teuren Berechnungen zwischenspeichern.

Wenn Sie alle Ergebnisse gleichzeitig im RAM speichern können, verwenden Sie diese Option, um list()die Ergebnisse des Generators in einer einfachen Liste zu materialisieren und damit zu arbeiten.

Aaron Digulla
quelle
23

Ich möchte eine andere Lösung für ein altes Problem anbieten

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)

Der Vorteil davon im Vergleich zu so etwas list(iterator)ist, dass dies O(1)Raumkomplexität ist und list(iterator)ist O(n). Der Nachteil ist, dass Sie diese Methode nicht verwenden können, wenn Sie nur Zugriff auf den Iterator haben, nicht jedoch auf die Funktion, die den Iterator erzeugt hat. Zum Beispiel mag es vernünftig erscheinen, Folgendes zu tun, aber es wird nicht funktionieren.

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

squares = IterableAdapter(lambda: g)

for x in squares: print(x)
for x in squares: print(x)
michaelsnowden
quelle
@ Dewfy Im ersten Snippet befindet sich der Generator in der Zeile "squares = ...". Generatorausdrücke verhalten sich genauso wie das Aufrufen einer Funktion, die Yield verwendet, und ich habe nur eine verwendet, weil sie weniger ausführlich ist als das Schreiben einer Funktion mit Yield für ein so kurzes Beispiel. Im zweiten Snippet habe ich FunctionWithYield als generator_factory verwendet, sodass es immer dann aufgerufen wird, wenn iter aufgerufen wird, dh wenn ich "für x in y" schreibe.
michaelsnowden
Gute Lösung. Dadurch wird ein zustandsloses iterierbares Objekt anstelle eines statusbehafteten Iteratorobjekts erstellt, sodass das Objekt selbst wiederverwendbar ist. Besonders nützlich, wenn Sie ein iterierbares Objekt an eine Funktion übergeben möchten und diese Funktion das Objekt mehrmals verwendet.
Cosyn
5

Wenn die Antwort von GrzegorzOledzki nicht ausreicht, könnten Sie sie wahrscheinlich verwenden send(), um Ihr Ziel zu erreichen. Weitere Informationen zu erweiterten Generatoren und Ertragsausdrücken finden Sie in PEP-0342 .

UPDATE: Siehe auch itertools.tee(). Es beinhaltet einen Teil des oben erwähnten Kompromisses zwischen Speicher und Verarbeitung, aber es kann etwas Speicherplatz sparen , wenn nur die Generatorergebnisse gespeichert werden list. Dies hängt davon ab, wie Sie den Generator verwenden.

Hank Gay
quelle
5

Wenn Ihr Generator in dem Sinne rein ist, dass seine Ausgabe nur von übergebenen Argumenten und der Schrittnummer abhängt und Sie möchten, dass der resultierende Generator neu gestartet werden kann, ist hier ein Sortierausschnitt, der nützlich sein kann:

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))

Ausgänge:

[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
Ben Usman
quelle
3

Aus der offiziellen Dokumentation des T-Stücks :

Wenn ein Iterator die meisten oder alle Daten verwendet, bevor ein anderer Iterator startet, ist es im Allgemeinen schneller, list () anstelle von tee () zu verwenden.

Verwenden Sie es list(iterable)stattdessen am besten in Ihrem Fall.

Shubham Chaudhary
quelle
6
Was ist mit unendlichen Generatoren?
Dewfy
1
Geschwindigkeit ist nicht die einzige Überlegung; list()legt das Ganze iterable in Erinnerung
Chris_Rands
@Chris_Rands tee()Wenn ein Iterator alle Werte verbraucht, teefunktioniert das auch.
AChampion
2
@ Dewfy: Verwenden Sie für unendliche Generatoren die Lösung von Aaron Digulla (ExpensiveSetup-Funktion, die die wertvollen Daten
zurückgibt
3

Verwenden einer Wrapper-Funktion zur Handhabung StopIteration

Sie können eine einfache Wrapper-Funktion in Ihre Generator-Generierungsfunktion schreiben, die nachverfolgt, wann der Generator erschöpft ist. Dies geschieht mit der StopIterationAusnahme, die ein Generator auslöst, wenn das Ende der Iteration erreicht ist.

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

Wie Sie oben sehen können, StopIterationinitialisiert unsere Wrapper-Funktion, wenn sie eine Ausnahme abfängt, das Generatorobjekt einfach neu (unter Verwendung einer anderen Instanz des Funktionsaufrufs).

Angenommen, Sie definieren Ihre generatorversorgende Funktion wie folgt: Verwenden Sie die Syntax des Python-Funktionsdekorators, um sie implizit zu verpacken:

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

Sie können eine Funktion definieren, die Ihren Generator zurückgibt

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

  return FunctionWithYield

Jetzt können Sie so oft tun, wie Sie möchten:

for x in f()(generator_args): print(x)
for x in f()(generator_args): print(x)
SMeznaric
quelle
1
Vielen Dank für die Antwort, aber der Hauptpunkt der Frage war, die Schöpfung zu vermeiden . Das Aufrufen der inneren Funktion verbirgt nur die Schöpfung - Sie erstellen sie zweimal
Dewfy
1

Ich bin mir nicht sicher, was Sie mit teurer Vorbereitung gemeint haben, aber ich denke, Sie haben es tatsächlich

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)

Wenn dies der Fall ist, warum nicht wiederverwenden data?

ilya n.
quelle
1

Es gibt keine Option zum Zurücksetzen von Iteratoren. Der Iterator wird normalerweise angezeigt, wenn er durchlaufen wirdnext() Funktion . Die einzige Möglichkeit besteht darin, vor dem Iterieren des Iteratorobjekts eine Sicherungskopie zu erstellen. Überprüfen Sie unten.

Erstellen eines Iteratorobjekts mit den Elementen 0 bis 9

i=iter(range(10))

Durchlaufen der Funktion next (), die herausspringt

print(next(i))

Konvertieren des Iteratorobjekts in eine Liste

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

Punkt 0 ist also bereits herausgesprungen. Außerdem werden alle Elemente angezeigt, wenn wir den Iterator in eine Liste konvertieren.

next(L) 

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

Sie müssen den Iterator also in Listen zur Sicherung konvertieren, bevor Sie mit der Iteration beginnen. Liste könnte mit in Iterator konvertiert werdeniter(<list-object>)

Amalraj Sieg
quelle
1

Sie können jetzt verwenden more_itertools.seekable (ein Drittanbieter-Tool) verwenden, mit dem Iteratoren zurückgesetzt werden können.

Installieren über > 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)

Hinweis: Der Speicherverbrauch steigt, während der Iterator weiterentwickelt wird. Seien Sie daher vorsichtig bei großen Iterables.

Pylang
quelle
1

Mit itertools.cycle () können Sie einen Iterator mit dieser Methode erstellen und dann eine for-Schleife über den Iterator ausführen, die seine Werte durchläuft .

Beispielsweise:

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

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

generiert 20 Zahlen, 0 bis 4 wiederholt.

Ein Hinweis aus den Dokumenten:

Note, this member of the toolkit may require significant auxiliary storage (depending on the length of the iterable).
SajanGohil
quelle
+1, weil es funktioniert, aber ich sehe dort 2 Probleme 1) großer Speicherbedarf, da in der Dokumentation angegeben ist "Kopie erstellen" 2) Endlosschleife ist definitiv nicht das, was ich will
Dewfy
0

Ok, Sie sagen, Sie möchten einen Generator mehrmals aufrufen, aber die Initialisierung ist teuer ... Was ist mit so etwas?

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

Alternativ können Sie auch eine eigene Klasse erstellen, die dem Iteratorprotokoll folgt und eine Art 'Reset'-Funktion definiert.

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

tvt173
quelle
Sie delegieren das Problem einfach an den Wrapper. Angenommen, die teure Initialisierung erzeugt einen Generator. Meine Frage war, wie man in Ihrem__call__
Dewfy
Als Antwort auf Ihren Kommentar wurde ein zweites Beispiel hinzugefügt. Dies ist im Wesentlichen ein benutzerdefinierter Generator mit einer Reset-Methode.
tvt173
0

Meine Antwort löst ein etwas anderes Problem: Wenn die Initialisierung des Generators teuer ist und die Generierung jedes generierten Objekts teuer ist. Wir müssen den Generator jedoch mehrmals in mehreren Funktionen verwenden. Um den Generator und jedes generierte Objekt genau einmal aufzurufen, können wir Threads verwenden und jede der konsumierenden Methoden in einem anderen Thread ausführen. Aufgrund von GIL erreichen wir möglicherweise keine echte Parallelität, aber wir werden unser Ziel erreichen.

Dieser Ansatz hat im folgenden Fall gute Arbeit geleistet: Das Deep-Learning-Modell verarbeitet viele Bilder. Das Ergebnis sind viele Masken für viele Objekte auf dem Bild. Jede Maske verbraucht Speicher. Wir haben ungefähr 10 Methoden, die unterschiedliche Statistiken und Metriken erstellen, aber alle Bilder gleichzeitig aufnehmen. Alle Bilder können nicht in den Speicher passen. Die Methoden können leicht umgeschrieben werden, um den Iterator zu akzeptieren.

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

Ussage:

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())
Asen
quelle
Sie erfinden nur neu itertools.isliceoder für Async aiostream.stream.take, und dieser Beitrag ermöglicht es Ihnen, dies auf asynchrone / wartende
Dewfy
-3

Dies kann durch ein Codeobjekt erfolgen. Hier ist das Beispiel.

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

OlegOS
quelle
4
Nun, tatsächlich musste der Generator zurückgesetzt werden, um die doppelte Ausführung des Initialisierungscodes zu vermeiden. Ihr Ansatz (1) führt die Initialisierung ohnehin zweimal durch, (2) es handelt sich um execeine etwas nicht empfohlene Initialisierung für einen solchen einfachen Fall.
Dewfy