Сброс генератора объекта в Python


153

У меня есть объект генератора, возвращаемый с помощью метода yield. Подготовка к вызову этого генератора довольно трудоемкая операция. Вот почему я хочу использовать генератор несколько раз.

y = FunctionWithYield()
for x in y: print(x)
#here must be something to reset 'y'
for x in y: print(x)

Конечно, я имею в виду копирование контента в простой список. Есть ли способ перезагрузить мой генератор?

Ответы:


119

Другой вариант - использовать itertools.tee()функцию для создания второй версии вашего генератора:

y = FunctionWithYield()
y, y_backup = tee(y)
for x in y:
    print(x)
for x in y_backup:
    print(x)

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


33
Если вам интересно, что он будет делать в этом случае, это по сути элементы кэширования в списке. Таким образом, вы можете использовать y = list(y)остальную часть кода без изменений.
Илья Н.

5
tee () создаст список для хранения данных, так что это то же самое, что я сделал в своем ответе.
Носкло

6
Посмотрите на имплементацию ( docs.python.org/library/itertools.html#itertools.tee ) - здесь используется стратегия отложенной загрузки, поэтому элементы в списке копируются только по требованию
Dewfy

11
@Dewfy: что будет медленнее, так как все элементы должны быть скопированы в любом случае.
nosklo

8
да, list () лучше в этом случае. тройник полезен, только если вы не используете весь список
гравитация

148

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

  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 является то, что он снова вычисляет значения. Если это сильно загружает процессор, вы в итоге рассчитываете дважды. С другой стороны, обратной стороной 2 является хранилище. Весь список значений будет храниться в памяти. Если значений слишком много, это может быть непрактично.

Таким образом, у вас есть классический компромисс между памятью и обработкой . Я не могу представить способ перемотки генератора без сохранения значений или их повторного вычисления.


Может быть, существует способ сохранить подпись вызова функции? FunctionWithYield, param1, param2 ...
Dewfy

3
@Dewfy: обязательно: def call_my_func (): вернуть FunctionWithYield (param1, param2)
nosklo

@Dewfy Что вы подразумеваете под «сохранить подпись вызова функции»? Не могли бы вы объяснить? Вы имеете в виду сохранение параметров, переданных в генератор?
Андрей Беньковский

2
Другим недостатком (1) также является то, что FunctionWithYield () может быть не только дорогостоящим, но и невозможно пересчитать, например, если он читает из стандартного ввода.
Макс

2
Чтобы повторить сказанное @Max, если выходные данные функции могут (или будут) изменяться между вызовами, (1) может дать неожиданные и / или нежелательные результаты.
Sam_Butler

36
>>> 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

29

Вероятно, самое простое решение - обернуть дорогую деталь в объект и передать ее генератору:

data = ExpensiveSetup()
for x in FunctionWithYield(data): pass
for x in FunctionWithYield(data): pass

Таким образом, вы можете кэшировать дорогие вычисления.

Если вы можете хранить все результаты в ОЗУ одновременно, используйте их list()для материализации результатов генератора в виде простого списка и работы с ним.


23

Я хочу предложить другое решение старой проблемы

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)

@Dewfy В первом фрагменте генератор находится в строке "squares = ...". Выражения генератора ведут себя так же, как и вызов функции, которая использует yield, и я использовал только одно, потому что это менее многословно, чем написание функции с yield для такого короткого примера. Во втором фрагменте я использовал FunctionWithYield в качестве генератора_фактора, поэтому он будет вызываться всякий раз, когда вызывается iter , то есть всякий раз, когда я пишу «для x в y».
Майклсноуден

Хорошее решение Это фактически делает итерируемый объект без сохранения состояния вместо итератора с сохранением состояния, так что сам объект можно использовать повторно. Особенно полезно, если вы хотите передать итерируемый объект в функцию, и эта функция будет использовать объект несколько раз.
Cosyn

5

Если ответа GrzegorzOledzki не будет достаточно, вы, вероятно, можете использовать send()для достижения своей цели. См. PEP-0342 для более подробной информации о расширенных генераторах и выражениях yield.

ОБНОВЛЕНИЕ: Также см itertools.tee(). Он включает в себя часть упомянутого выше компромисса между памятью и обработкой, но это может сэкономить некоторую память по сравнению с простым хранением генератора, что приводит к list; это зависит от того, как вы используете генератор.


5

Если ваш генератор является чистым в том смысле, что его вывод зависит только от переданных аргументов и номера шага, и вы хотите, чтобы полученный генератор был перезапускаемым, вот фрагмент сортировки, который может быть полезен:

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

3

Из официальной документации тройника :

В общем, если один итератор использует большую часть или все данные перед запуском другого итератора, быстрее использовать list () вместо tee ().

Так что лучше использовать list(iterable)вместо этого в вашем случае.


6
как насчет бесконечных генераторов?
Dewfy

1
Скорость не единственное соображение; list()помещает все повторяемое в память
Chris_Rands

@Chris_Rands Так будет, tee()если один итератор использует все значения - вот как это teeработает.
Чемпион Февраля

2
@Dewfy: для бесконечных генераторов используйте решение Аарона Дигуллы (функция ExорогоSetup, возвращающая ценные данные.)
Джефф Лирман,

3

Использование функции-оболочки для обработки 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

2

Вы можете определить функцию, которая возвращает ваш генератор

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)

1
Спасибо за ответ, но основной вопрос заключался в том, чтобы избежать создания , поскольку внутренняя функция просто скрывает создание - вы создаете его дважды
Dewfy

1

Я не уверен, что вы имели в виду под дорогостоящим препаратом, но я думаю, у вас действительно есть

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?


1

Там нет опции для сброса итераторов. Итератор обычно выскакивает, когда он перебирает next()функцию. Единственный способ - сделать резервную копию перед итерацией на объекте итератора. Проверьте ниже.

Создание объекта итератора с элементами от 0 до 9

i=iter(range(10))

Итерация по функции next (), которая появится

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>)


1

Теперь вы можете использовать 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)

Примечание: потребление памяти увеличивается при продвижении итератора, так что будьте осторожны с большими итерациями.


1

Вы можете сделать это, используя itertools.cycle (), вы можете создать итератор с помощью этого метода, а затем выполнить цикл for для итератора, который зациклит его значения.

Например:

def generator():
for j in cycle([i for i in range(5)]):
    yield j

gen = generator()
for i in range(20):
    print(next(gen))

сгенерирует 20 чисел, от 0 до 4 раз.

Примечание от документов:

Note, this member of the toolkit may require significant auxiliary storage (depending on the length of the iterable).

+1, потому что это работает, но я вижу 2 проблемы там 1) большой объем памяти, так как в документации говорится «создать копию» 2) бесконечный цикл - определенно не то, что я хочу
Dewfy

0

Хорошо, вы говорите, что хотите вызывать генератор несколько раз, но инициализация стоит дорого ... Как насчет этого?

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


Вы просто делегируете проблему оболочке. Предположим, что дорогая инициализация создает генератор. Мой вопрос был о том, как выполнить сброс внутри__call__
Dewfy

Добавлен второй пример в ответ на ваш комментарий. Это по сути пользовательский генератор с методом сброса.
tvt173

0

Мой ответ решает немного другую проблему: если генератор дорог для инициализации, а каждый сгенерированный объект дорог для генерации. Но нам нужно использовать генератор несколько раз в нескольких функциях. Чтобы вызвать генератор и каждый сгенерированный объект ровно один раз, мы можем использовать потоки и запускать каждый из потребляющих методов в разных потоках. Мы можем не достичь истинного параллелизма благодаря GIL, но мы достигнем нашей цели.

Этот подход хорошо зарекомендовал себя в следующем случае: модель глубокого обучения обрабатывает много изображений. В результате получается множество масок для множества объектов на изображении. Каждая маска потребляет память. У нас есть около 10 методов, которые делают разные статистические данные и метрики, но они берут все изображения одновременно. Все изображения не могут поместиться в памяти. Моеходы могут быть легко переписаны, чтобы принять итератор.

class GeneratorSplitter:
'''
Split a generator object into multiple generators which will be sincronised. Each call to each of the sub generators will cause only one call in the input generator. This way multiple methods on threads can iterate the input generator , and the generator will cycled only once.
'''

def __init__(self, gen):
    self.gen = gen
    self.consumers: List[GeneratorSplitter.InnerGen] = []
    self.thread: threading.Thread = None
    self.value = None
    self.finished = False
    self.exception = None

def GetConsumer(self):
    # Returns a generator object. 
    cons = self.InnerGen(self)
    self.consumers.append(cons)
    return cons

def _Work(self):
    try:
        for d in self.gen:
            for cons in self.consumers:
                cons.consumed.wait()
                cons.consumed.clear()

            self.value = d

            for cons in self.consumers:
                cons.readyToRead.set()

        for cons in self.consumers:
            cons.consumed.wait()

        self.finished = True

        for cons in self.consumers:
            cons.readyToRead.set()
    except Exception as ex:
        self.exception = ex
        for cons in self.consumers:
            cons.readyToRead.set()

def Start(self):
    self.thread = threading.Thread(target=self._Work)
    self.thread.start()

class InnerGen:
    def __init__(self, parent: "GeneratorSplitter"):
        self.parent: "GeneratorSplitter" = parent
        self.readyToRead: threading.Event = threading.Event()
        self.consumed: threading.Event = threading.Event()
        self.consumed.set()

    def __iter__(self):
        return self

    def __next__(self):
        self.readyToRead.wait()
        self.readyToRead.clear()
        if self.parent.finished:
            raise StopIteration()
        if self.parent.exception:
            raise self.parent.exception
        val = self.parent.value
        self.consumed.set()
        return val

Ussage:

genSplitter = GeneratorSplitter(expensiveGenerator)

metrics={}
executor = ThreadPoolExecutor(max_workers=3)
f1 = executor.submit(mean,genSplitter.GetConsumer())
f2 = executor.submit(max,genSplitter.GetConsumer())
f3 = executor.submit(someFancyMetric,genSplitter.GetConsumer())
genSplitter.Start()

metrics.update(f1.result())
metrics.update(f2.result())
metrics.update(f3.result())

Вы просто заново изобрели itertools.isliceили для асинхронного aiostream.stream.take, и этот пост позволяет вам сделать это asyn / await способом stackoverflow.com/a/42379188/149818
Dewfy

-3

Это может быть сделано с помощью объекта кода. Вот пример.

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


4
ну, собственно, сброс генератора был необходим, чтобы избежать двойного выполнения кода инициализации. Ваш подход (1) в любом случае выполняет инициализацию дважды, (2) он включает в себя execто, что немного не рекомендуется для такого простого случая.
Dewfy
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.