Сброс объекта генератора в 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 заключается в том, что он снова вычисляет значения. Если это CPU-intensive, вы в конечном итоге вычисляете дважды. На с другой стороны, обратная сторона 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)
Если ответ GrzegorzOledzki не достаточно, вы могли бы использовать 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
Это может быть сделано объектом кода. Вот пример.
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 Два Три 4
for i in y: print i
exec(code1)
for i in y: print i
1 Два Три 4