Resetowanie obiektu generatora w Pythonie


153

Mam obiekt generatora zwrócony przez wielokrotną wydajność. Przygotowanie do wywołania tego generatora jest raczej czasochłonną operacją. Dlatego chcę kilka razy ponownie użyć generatora.

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

Oczywiście myślę o skopiowaniu treści na prostą listę. Czy jest sposób na zresetowanie mojego generatora?

Odpowiedzi:


119

Inną opcją jest użycie itertools.tee()funkcji do stworzenia drugiej wersji twojego generatora:

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

Może to być korzystne z punktu widzenia wykorzystania pamięci, jeśli oryginalna iteracja może nie przetworzyć wszystkich elementów.


33
Jeśli zastanawiasz się, co zrobi w tym przypadku, to zasadniczo buforuje elementy na liście. Więc równie dobrze możesz użyć y = list(y)z pozostałą częścią swojego kodu bez zmian.
ilya n.

5
Tee () utworzy wewnętrznie listę do przechowywania danych, więc jest to to samo, co w mojej odpowiedzi.
nosklo

6
Spójrz na implementację ( docs.python.org/library/itertools.html#itertools.tee ) - używa strategii leniwego ładowania, więc elementy do listy kopiowane tylko na żądanie
Dewfy

11
@Dewfy: Co będzie wolniejsze, ponieważ wszystkie elementy i tak będą musiały zostać skopiowane.
nosklo

8
tak, lista () jest lepsza w tym przypadku. tee przydaje się tylko wtedy, gdy nie zużywasz całej listy
grawitacja

148

Generatorów nie można przewijać. Masz następujące możliwości:

  1. Uruchom ponownie funkcję generatora, ponownie uruchamiając generowanie:

    y = FunctionWithYield()
    for x in y: print(x)
    y = FunctionWithYield()
    for x in y: print(x)
  2. Przechowywanie generatora powoduje powstanie struktury danych w pamięci lub na dysku, którą można powtórzyć:

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

Wadą opcji 1 jest ponowne obliczenie wartości. Jeśli wymaga to dużej mocy obliczeniowej, w końcu obliczasz dwa razy. Z drugiej strony wadą 2 jest przechowywanie. Cała lista wartości zostanie zapisana w pamięci. Jeśli wartości jest zbyt wiele, może to być niepraktyczne.

Masz więc klasyczny kompromis między pamięcią a przetwarzaniem . Nie wyobrażam sobie sposobu na przewinięcie generatora bez przechowywania wartości lub ich ponownego obliczania.


Może istnieje sposób na zapisanie sygnatury wywołania funkcji? FunctionWithYield, param1, param2 ...
Dewfy

3
@Dewfy: sure: def call_my_func (): return FunctionWithYield (param1, param2)
nosklo

@Dewfy Co masz na myśli mówiąc „zapisz podpis wywołania funkcji”? Czy mógłbyś wyjaśnić? Masz na myśli zapisanie parametrów przekazanych do generatora?
Андрей Беньковский

2
Inną wadą (1) jest również to, że FunctionWithYield () może być nie tylko kosztowne, ale także niemożliwe do ponownego obliczenia, np. Jeśli odczytuje ze standardowego wejścia.
Max

2
Aby powtórzyć to, co powiedział @Max, jeśli wyjście funkcji może (lub będzie) zmieniać się między wywołaniami, (1) może dać nieoczekiwane i / lub niepożądane wyniki.
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

Prawdopodobnie najprostszym rozwiązaniem jest owinięcie drogiej części w obiekt i przekazanie go do generatora:

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

W ten sposób możesz buforować kosztowne obliczenia.

Jeśli możesz przechowywać wszystkie wyniki w pamięci RAM w tym samym czasie, użyj, list()aby zmaterializować wyniki generatora na zwykłej liście i pracuj z tym.


23

Chcę zaproponować inne rozwiązanie starego problemu

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)

Zaletą tego w porównaniu z czymś podobnym list(iterator)jest to, że jest to O(1)złożoność przestrzeni i list(iterator)jest O(n). Wadą jest to, że jeśli masz dostęp tylko do iteratora, ale nie do funkcji, która utworzyła iterator, nie możesz użyć tej metody. Na przykład wykonanie poniższych czynności może wydawać się rozsądne, ale nie zadziała.

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 W pierwszym fragmencie generator znajduje się w wierszu „squares = ...”. Wyrażenia generatora zachowują się tak samo, jak wywołanie funkcji, która używa yield, a ja użyłem tylko jednej, ponieważ jest mniej szczegółowa niż pisanie funkcji z yieldem dla tak krótkiego przykładu. W drugim fragmencie użyłem FunctionWithYield jako generator_factory, więc będzie on wywoływany za każdym razem, gdy zostanie wywołany iter , czyli za każdym razem, gdy napiszę „for x in y”.
michaelsnowden

Dobre rozwiązanie. W rzeczywistości powoduje to, że obiekt iterowalny bezstanowy zamiast stanowego obiektu iteratora, więc sam obiekt jest wielokrotnego użytku. Jest to szczególnie przydatne, jeśli chcesz przekazać iterowalny obiekt do funkcji, a ta funkcja będzie używać tego obiektu wiele razy.
Cosyn,

5

Jeśli odpowiedź Grzegorza Oledzkiego nie wystarczy, prawdopodobnie przydałbyś się send()do osiągnięcia celu. Zobacz PEP-0342 więcej szczegółów na temat rozszerzonych generatorów i wyrażeń dochodowości.

UPDATE: Zobacz także itertools.tee(). Obejmuje to część wspomnianego powyżej kompromisu między pamięcią a przetwarzaniem, ale może zaoszczędzić trochę pamięci w porównaniu z samym przechowywaniem wyników generatora w list; zależy to od tego, jak używasz generatora.


5

Jeśli twój generator jest czysty w tym sensie, że jego wyjście zależy tylko od przekazanych argumentów i numeru kroku, a chcesz, aby wynikowy generator był uruchamiany ponownie, oto fragment kodu sortowania, który może być przydatny:

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

wyjścia:

[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

Z oficjalnej dokumentacji koszulki :

Ogólnie rzecz biorąc, jeśli jeden iterator używa większości lub wszystkich danych przed uruchomieniem innego iteratora, szybsze jest użycie list () zamiast tee ().

Dlatego najlepiej jest użyć list(iterable)zamiast tego w twoim przypadku.


6
a co z nieskończonymi generatorami?
Dewfy,

1
Szybkość nie jest jedyną kwestią; list()
zapisuje

@Chris_Rands Tak samo będzie, tee()jeśli jeden iterator zużyje wszystkie wartości - tak to teedziała.
AChampion

2
@Dewfy: w przypadku nieskończonych generatorów użyj rozwiązania Aarona Digulli (funkcja ExpensiveSetup zwraca cenne dane.)
Jeff Learman,

3

Używanie funkcji opakowującej do obsługi StopIteration

Możesz napisać prostą funkcję opakowującą do funkcji generującej generator, która śledzi, kiedy generator jest wyczerpany. Zrobi to przy użyciu StopIterationwyjątku generowanego przez generator, gdy osiągnie koniec iteracji.

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

Jak widać powyżej, kiedy nasza funkcja opakowująca przechwytuje plik StopIteration wyjątek, po prostu ponownie inicjalizuje obiekt generatora (używając innej instancji wywołania funkcji).

A potem, zakładając, że zdefiniujesz swoją funkcję dostarczającą generator gdzieś, jak poniżej, możesz użyć składni dekoratora funkcji Pythona, aby ją zawinąć niejawnie:

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

2

Możesz zdefiniować funkcję, która zwraca generator

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

  return FunctionWithYield

Teraz możesz po prostu zrobić tyle razy, ile chcesz:

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

1
Dziękuję za odpowiedź, ale głównym pytaniem było unikanie tworzenia , wywołanie funkcji wewnętrznej po prostu ukrywa kreację - tworzysz ją dwukrotnie
Dewfy

1

Nie jestem pewien, co miałeś na myśli mówiąc o drogim przygotowaniu, ale myślę, że tak naprawdę masz

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)

Jeśli tak jest, dlaczego nie wykorzystać ponownie data?


1

Nie ma opcji resetowania iteratorów. Iterator zwykle wyskakuje podczas iteracjinext() funkcję. Jedynym sposobem jest wykonanie kopii zapasowej przed iteracją na obiekcie iteratora. Sprawdź poniżej.

Tworzenie obiektu iteratora z elementami od 0 do 9

i=iter(range(10))

Iterowanie po funkcji next (), która wyskoczy

print(next(i))

Konwersja obiektu iteratora na listę

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

więc element 0 już wyskoczył. Również wszystkie elementy są pobierane, gdy konwertowaliśmy iterator na listę.

next(L) 

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

Musisz więc przekonwertować iterator na listy do tworzenia kopii zapasowych przed rozpoczęciem iteracji. Listę można przekonwertować na iterator ziter(<list-object>)


1

Możesz teraz użyć more_itertools.seekable (narzędzia innej firmy), które umożliwia resetowanie iteratorów.

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

Uwaga: zużycie pamięci rośnie wraz z rozwojem iteratora, więc uważaj na duże iteracje.


1

Możesz to zrobić, używając itertools.cycle () , możesz utworzyć iterator za pomocą tej metody, a następnie wykonać pętlę for na iteratorze, który zapętli jego wartości.

Na przykład:

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

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

wygeneruje 20 liczb, od 0 do 4 wielokrotnie.

Notatka z dokumentów:

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

+1, ponieważ to działa, ale widzę tam 2 problemy 1) duży ślad pamięciowy, ponieważ dokumentacja stwierdza "utwórz kopię" 2) Nieskończona pętla zdecydowanie nie jest tym, czego chcę
Dewfy

0

Ok, mówisz, że chcesz wielokrotnie wywoływać generator, ale inicjalizacja jest droga ... A co z czymś takim?

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

Alternatywnie, możesz po prostu stworzyć własną klasę, która będzie zgodna z protokołem iteratora i definiuje jakąś funkcję „resetowania”.

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


Po prostu delegujesz problem do wrappera. Załóżmy, że kosztowna inicjalizacja tworzy generator. Moje pytanie dotyczyło resetowania w twoim__call__
Dewfy

Dodano drugi przykład w odpowiedzi na Twój komentarz. Zasadniczo jest to niestandardowy generator z metodą resetowania.
tvt173

0

Moja odpowiedź rozwiązuje nieco inny problem: jeśli inicjalizacja generatora jest kosztowna, a wygenerowanie każdego wygenerowanego obiektu jest drogie. Ale musimy wielokrotnie zużywać generator w wielu funkcjach. Aby wywołać generator i każdy wygenerowany obiekt dokładnie raz, możemy użyć wątków i uruchomić każdą zużywającą się metodę w innym wątku. Możemy nie osiągnąć prawdziwego paralelizmu dzięki GIL, ale osiągniemy nasz cel.

Takie podejście sprawdziło się w następującym przypadku: model głębokiego uczenia przetwarza wiele obrazów. Rezultatem jest wiele masek dla wielu obiektów na obrazie. Każda maska ​​zajmuje pamięć. Mamy około 10 metod, które tworzą różne statystyki i metryki, ale pobierają wszystkie obrazy naraz. Wszystkie obrazy nie mieszczą się w pamięci. Metody można łatwo przepisać, aby akceptowały iterator.

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

Zastosowanie:

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

Po prostu wymyślasz na nowo itertools.islicelub asynchronicznie aiostream.stream.take, a ten post pozwala ci to zrobić w sposób asyn / await stackoverflow.com/a/42379188/149818
Dewfy

-3

Można to zrobić za pomocą obiektu kodu. Oto przykład.

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
cóż, faktycznie konieczne było zresetowanie generatora, aby uniknąć dwukrotnego wykonania kodu inicjalizacyjnego. Twoje podejście (1) i tak wykonuje inicjalizację dwukrotnie, (2) wymaga exectego nieco niezalecane w tak prostym przypadku.
Dewfy
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.