Generatorobject resetten in Python

Ik heb een generatorobject geretourneerd met meervoudige opbrengst. Voorbereiding om deze generator aan te roepen is nogal tijdrovend. Daarom wil ik de generator meerdere keren hergebruiken.

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

Natuurlijk houd ik rekening met het kopiëren van inhoud naar een eenvoudige lijst. Is er een manier om mijn generator te resetten?


Antwoord 1, autoriteit 100%

Generatoren kunnen niet worden teruggespoeld. Je hebt de volgende opties:

  1. Voer de generatorfunctie opnieuw uit en herstart de generatie:

    y = FunctionWithYield()
    for x in y: print(x)
    y = FunctionWithYield()
    for x in y: print(x)
    
  2. Sla de generatorresultaten op in een gegevensstructuur op geheugen of schijf die u opnieuw kunt herhalen:

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

Het nadeel van optie 1is dat het de waarden opnieuw berekent. Als dat CPU-intensief is, reken je uiteindelijk twee keer. Aan de andere kant is het nadeel van 2de opslag. De volledige lijst met waarden wordt in het geheugen opgeslagen. Als er te veel waarden zijn, kan dat onpraktisch zijn.

Dus je hebt de klassieke afweging geheugen versus verwerking. Ik kan me geen manier voorstellen om de generator terug te spoelen zonder de waarden op te slaan of ze opnieuw te berekenen.


Antwoord 2, autoriteit 84%

Een andere optie is om de itertools.tee()functie om een ​​tweede versie van uw generator te maken:

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

Dit kan gunstig zijn vanuit het oogpunt van geheugengebruik als de oorspronkelijke iteratie mogelijk niet alle items verwerkt.


Antwoord 3, autoriteit 24%

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

Antwoord 4, autoriteit 18%

Waarschijnlijk is de meest eenvoudige oplossing om het dure onderdeel in een object te wikkelen en dat door te geven aan de generator:

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

Op deze manier kunt u de dure berekeningen cachen.

Als je alle resultaten tegelijkertijd in RAM kunt bewaren, gebruik dan list()om de resultaten van de generator in een duidelijke lijst te materialiseren en daarmee te werken.


Antwoord 5, autoriteit 16%

Ik wil een andere oplossing bieden voor een oud probleem

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)

Het voordeel hiervan in vergelijking met iets als list(iterator)is dat dit O(1)ruimtecomplexiteit en list(iterator)is O(n). Het nadeel is dat, als je alleen toegang hebt tot de iterator, maar niet de functie die de iterator heeft geproduceerd, je deze methode niet kunt gebruiken. Het lijkt bijvoorbeeld redelijk om het volgende te doen, maar het zal niet werken.

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

Antwoord 6, autoriteit 4%

Een wrapper-functie gebruiken om StopIteration

af te handelen

Je zou een eenvoudige wrapper-functie naar je generator-genererende functie kunnen schrijven die bijhoudt wanneer de generator is uitgeput. Het zal dit doen met behulp van de StopIteration-uitzondering die een generator genereert wanneer het einde van de iteratie bereikt.

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

Zoals je hierboven kunt zien, initialiseert onze wrapper-functie een StopIteration-uitzondering, simpelweg opnieuw het generatorobject (met een andere instantie van de functieaanroep).

En dan, ervan uitgaande dat je je generator-leverende functie ergens definieert zoals hieronder, zou je de Python-functie-decoratorsyntaxis kunnen gebruiken om het impliciet in te pakken:

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

Antwoord 7, autoriteit 3%

Als het antwoord van GrzegorzOledzki niet voldoende is, kunt u waarschijnlijk send()gebruiken om uw doel te bereiken. Zie PEP-0342voor meer details over verbeterde generatoren en opbrengstexpressies.

UPDATE: zie ook itertools.tee(). Het gaat om een ​​deel van de eerder genoemde afweging tussen geheugen en verwerking, maar het kanwat geheugen besparen door alleen de generatorresultaten op te slaan in een list; het hangt af van hoe je de generator gebruikt.


Antwoord 8, autoriteit 3%

Als je generator puur is in die zin dat de output alleen afhangt van doorgegeven argumenten en het stapnummer, en je wilt dat de resulterende generator herstartbaar is, dan is hier een sorteerfragment dat handig kan zijn:

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

uitgangen:

[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

Antwoord 9, autoriteit 2%

van officiële documentatie van tee :

In het algemeen, als een iterator de meeste of alle gegevens gebruikt
Een andere iterator begint, het is sneller om lijst () in plaats van tee () te gebruiken.

Dus het is het beste om list(iterable)in uw geval te gebruiken.


Antwoord 10

U kunt een functie definiëren die uw generator retourneert

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

Nu kunt u net zo vaak doen zoals u wilt:

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

Antwoord 11

Ik weet niet zeker wat je bedoelde met dure voorbereiding, maar ik denk dat je eigenlijk

hebt

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)

Als dat het geval is, waarom niet opnieuw gebruiken data?


Antwoord 12

Er is geen optie om iterators opnieuw in te stellen. Iterator springt meestal uit wanneer het wordt geïntegreerd via next()-functie. Enige manier is om een ​​back-up te maken voordat het op het iteratorobject is. Controleer hieronder.

iTerator-object maken met items 0 tot 9

i=iter(range(10))

iterating via de volgende () functie die wordt uitgeschakeld

print(next(i))

Het iteratorobject converteren naar lijst

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

Zo is item 0 al uitgekomen. Ook zijn alle items geplooid als we de iterator naar lijst omgezet.

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

Dus u moet de iterator naar lijsten omzetten voor back-up voordat u begint met iteratie.
Lijst kan worden geconverteerd naar iterator met iter(<list-object>)


Antwoord 13

U kunt nu more_itertools.seekable(een gereedschap van derden) waarmee het resetten van iterators.

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

OPMERKING: Het geheugenverbruik groeit terwijl u de iterator bevordert, dus wees op uw hoede voor grote iterables.


Antwoord 14

U kunt dat doen met behulp van itertools.cycle ()
U kunt een iterator maken met deze methode en vervolgens een voor lus over de iterator uitvoeren die in zijn waarden loopt.

Bijvoorbeeld:

def generator():
for j in cycle([i for i in range(5)]):
    yield j
gen = generator()
for i in range(20):
    print(next(gen))

genereert 20 nummers, 0 tot 4 herhaaldelijk.

Een notitie van de documenten:

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

Antwoord 15

Hoe het voor mij werkt.

csv_rows = my_generator()
for _ in range(10):
    for row in csv_rows:
        print(row)
    csv_rows = my_generator()

Antwoord 16

Ok, je zegt dat je een generator meerdere keren wilt aanroepen, maar initialisatie is duur… Hoe zit het met zoiets?

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

Je kunt ook gewoon je eigen klasse maken die het iteratorprotocol volgt en een soort ‘reset’-functie definieert.

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


Antwoord 17

Mijn antwoord lost een iets ander probleem op: als de generator duur is om te initialiseren en elk gegenereerd object duur om te genereren. Maar we moeten de generator meerdere keren gebruiken in meerdere functies. Om de generator en elk gegenereerd object precies één keer aan te roepen, kunnen we threads gebruiken en elk van de consumerende methoden in verschillende threads uitvoeren. We bereiken misschien geen echt parallellisme vanwege GIL, maar we zullen ons doel bereiken.

Deze aanpak heeft goed gewerkt in het volgende geval: het deep learning-model verwerkt veel afbeeldingen. Het resultaat is veel maskers voor veel objecten op de afbeelding. Elk masker verbruikt geheugen. We hebben ongeveer 10 methoden die verschillende statistieken en statistieken maken, maar ze nemen alle afbeeldingen tegelijk. Alle afbeeldingen passen niet in het geheugen. De moethods kunnen gemakkelijk worden herschreven om de iterator te accepteren.

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

Gebruik:

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

Antwoord 18

Het kan worden gedaan door een code-object. Hier is het voorbeeld.

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

Other episodes