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

Это может быть полезно с точки зрения использования памяти, если исходная итерация может не обрабатывать все элементы.


генераторы не могут быть перемотаны. У вас есть следующие варианты:

  1. снова запустите функцию генератора, перезапустив генерацию:

    y = FunctionWithYield()
    for x in y: print(x)
    y = FunctionWithYield()
    for x in y: print(x)
    
  2. сохраните результаты генератора в структуре данных на памяти или диске, которую вы можете повторить снова:

    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