Сброс объекта генератора в Python
У меня есть объект генератора, возвращаемый несколькими выходами. Подготовка к вызову этого генератора является достаточно трудоемкой операцией. Вот почему я хочу повторно использовать генератор несколько раз.
y = FunctionWithYield()
for x in y: print(x)
#here must be something to reset 'y'
for x in y: print(x)
конечно, я имею в виду копирование контента в простой список.
15 ответов:
другой вариант-использовать
itertools.tee()
функция для создания второй версии вашего генератора:y = FunctionWithYield() y, y_backup = tee(y) for x in y: print(x) for x in y_backup: print(x)
Это может быть полезно с точки зрения использования памяти, если исходная итерация может не обрабатывать все элементы.
генераторы не могут быть перемотаны. У вас есть следующие варианты:
снова запустите функцию генератора, перезапустив генерацию:
y = FunctionWithYield() for x in y: print(x) y = FunctionWithYield() for x in y: print(x)
сохраните результаты генератора в структуре данных на памяти или диске, которую вы можете повторить снова:
y = list(FunctionWithYield()) for x in y: print(x) # can iterate again: for x in y: print(x)
обратная сторона опции 1 это то, что он вычисляет значения снова. Если это интенсивный процессор, вы в конечном итоге вычисляете дважды. На с другой стороны, обратная сторона 2 - это хранение. Весь список значений будет сохранен в памяти. Если существует слишком много значений, это может быть непрактично.
Итак, у вас есть классический память и обработку компромисс. Я не могу представить себе способ перемотки генератора без сохранения значений или их вычисления снова.
>>> 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
вероятно, самое простое решение-обернуть дорогостоящую часть в объект и передать ее генератору:
data = ExpensiveSetup() for x in FunctionWithYield(data): pass for x in FunctionWithYield(data): pass
таким образом, вы можете кэшировать дорогие расчеты.
Если вы можете сохранить все результаты в оперативной памяти в то же время, а затем использовать
list()
материализовать результаты работы генератора в простой список и работать с этим.
Я хочу предложить другое решение старой проблемы
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)
если ответ Гжегожоледзки не будет достаточным, вы, вероятно, могли бы использовать
send()
для достижения вашей цели. Смотрите PEP-0342 для получения более подробной информации о расширенных генераторах и выражениях выхода.обновление: Также см.
itertools.tee()
. Он включает в себя некоторые из этих компромиссов памяти и обработки, упомянутых выше, но это может сохраните некоторую память над просто хранением результатов генератора вlist
; это зависит от того, как вы используете генератор.
Если ваш генератор чист в том смысле, что его выход зависит только от переданных аргументов и номера шага, и вы хотите, чтобы результирующий генератор был перезапущен, вот фрагмент сортировки, который может быть полезен:
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
Вы можете определить функцию, которая возвращает генератор
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)
С официальная документация tee:
В общем случае, если один итератор использует большую часть или все данные до запускается другой итератор, быстрее использовать list () вместо tee ().
Так что лучше всего использовать
list(iterable)
, а не в вашем случае.
Теперь вы можете использовать
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)
Примечание: потребление памяти растет при продвижении итератора, поэтому будьте осторожны с большими итерациями.
Я не уверен, что вы имели в виду под дорогой подготовкой, но я думаю, что вы на самом деле есть
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
?
хорошо, вы говорите, что хотите вызвать генератор несколько раз, но инициализация стоит дорого... А как насчет чего-то вроде этого?
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
использование функции-оболочки для обработки
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