Сброс объекта генератора в 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