Сброс объекта генератора в Python
У меня есть объект-генератор, возвращаемый несколькими выходными данными. Подготовка к вызову этого генератора довольно трудоемкая операция. Вот почему я хочу несколько раз использовать генератор.
y = FunctionWithYield()
for x in y: print(x)
#here must be something to reset 'y'
for x in y: print(x)
Конечно, я принимаю во внимание копирование контента в простой список.
Ответы
Ответ 1
Другой вариант - использовать itertools.tee()
для создания второй версии вашего генератора:
y = FunctionWithYield()
y, y_backup = tee(y)
for x in y:
print(x)
for x in y_backup:
print(x)
Это может быть полезно с точки зрения использования памяти, если исходная итерация может не обрабатывать все элементы.
Ответ 2
Генераторы нельзя перематывать. У вас есть следующие возможности:
-
Запустите функцию генератора снова, перезапустив генерацию:
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-интенсивный вы заканчиваете вычислять дважды. С другой стороны, недостатком 2 является хранилище. Весь список значений будет сохранен в памяти. Если слишком много значений, это может быть непрактичным.
Итак, у вас есть классическая комбинация памяти и обработки. Я не могу представить себе способ перемотки генератора без сохранения значений или вычисления их снова.
Ответ 3
>>> 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
Ответ 4
Возможно, самым простым решением является обернуть дорогостоящую деталь в объект и передать это генератору:
data = ExpensiveSetup()
for x in FunctionWithYield(data): pass
for x in FunctionWithYield(data): pass
Таким образом, вы можете кэшировать дорогостоящие вычисления.
Если вы можете одновременно сохранить все результаты в ОЗУ, используйте list()
, чтобы материализовать результаты генератора в обычном списке и работать с ним.
Ответ 5
Я хочу предложить другое решение старой проблемы
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)
Ответ 6
Если ответ GrzegorzOledzki не будет достаточным, вы могли бы использовать send()
для достижения своей цели. См. PEP-0342 для получения дополнительной информации об улучшенных генераторах и выражениях yield.
UPDATE: см. itertools.tee()
. Он включает в себя некоторые из упомянутых выше соображений по сравнению с обработкой памяти, но он может сэкономить некоторую память, просто сохранив результаты генератора в list
; это зависит от того, как вы используете генератор.
Ответ 7
От официальная документация по тройнику:
В общем случае, если один итератор использует большинство или все данные до запускается другой итератор, быстрее использовать список() вместо tee().
Так что лучше использовать list(iterable)
вместо этого в вашем случае.
Ответ 8
Если ваш генератор является чистым в том смысле, что его вывод зависит только от переданных аргументов и номера шага, и вы хотите, чтобы результирующий генератор был перезапущен, вот фрагмент сортировки, который может быть полезен:
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
Ответ 9
Вы можете определить функцию, которая возвращает ваш генератор
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)
Ответ 10
Итераторов reset нет. Итератор обычно появляется, когда он выполняет итерацию через функцию next()
. Единственный способ - сделать резервную копию перед повторением итератора. Проверьте ниже.
Создание объекта итератора с элементами от 0 до 9
i=iter(range(10))
Итерация через следующую() функцию, которая выскочит
print(next(i))
Преобразование объекта итератора в список
L=list(i)
print(L)
output: [1, 2, 3, 4, 5, 6, 7, 8, 9]
так что элемент 0 уже выскочил. Также все элементы отображаются, когда мы перечислили итератор для списка.
next(L)
Traceback (most recent call last):
File "<pyshell#129>", line 1, in <module>
next(L)
StopIteration
Поэтому вам нужно преобразовать итератор в списки для резервного копирования, прежде чем запускать итерацию.
Список может быть преобразован в итератор с помощью iter(<list-object>)
Ответ 11
Теперь вы можете использовать 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)
Примечание: потребление памяти растет при продвижении итератора, поэтому будьте осторожны с большими итерациями.
Ответ 12
Использование функции-обертки для обработки 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
Ответ 13
Я не уверен, что вы имели в виду при помощи дорогостоящей подготовки, но я полагаю, что у вас действительно есть
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
?
Ответ 14
Хорошо, вы говорите, что хотите вызвать генератор несколько раз, но инициализация дорогая... Как насчет чего-то подобного?
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
В качестве альтернативы вы можете просто создать свой собственный класс, который следует за протоколом итератора и определит какую-то функцию reset.
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
Ответ 15
Это можно сделать с помощью объекта кода. Вот пример.
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