Скидання об'єкта генератора в Python


153

У мене є об'єкт генератора, повернутий із кратною врожайністю. Підготовка до виклику цього генератора - досить трудомістка операція. Ось чому я хочу повторно використовувати генератор.

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 () створить внутрішній список для зберігання даних, тож це те саме, що я зробив у своїй відповіді.
nosklo

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

11
@Dewfy: Що буде повільніше, оскільки всі елементи все одно доведеться копіювати.
nosklo

8
так, список () краще в цьому випадку. Трій корисний лише якщо ви не споживаєте весь список
гравітація

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 є те, що він обчислює значення знову. Якщо це CPU-інтенсивно, ви закінчите обчислення вдвічі. З іншого боку, недоліком 2 є сховище. Весь список значень буде зберігатися в пам'яті. Якщо занадто багато значень, це може бути непрактично.

Таким чином, у вас є класична пам'ять проти обробки компромісів . Я не уявляю способу перемотування генератора, не зберігаючи значення або обчислюючи їх знову.


Може існувати спосіб збереження підпису виклику функції? FunctionWithYield, param1, param2 ...
Dewfy

3
@Dewfy: sure: def call_my_func (): функція поверненняWithYield (param1, param2)
nosklo

@Dewfy Що ви маєте на увазі під "збереженням підпису виклику функції"? Не могли б ви пояснити? Ви маєте на увазі збереження параметрів, переданих генератору?
Андрій Беньковський

2
Іншим недоліком (1) є також те, що FunctionWithYield () може бути не тільки дорогим, але й неможливо перерахувати, наприклад, якщо він читає з stdin.
Макс

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 У першому фрагменті генератор знаходиться на рядку "квадрати = ...". Генераторні вирази поводяться так само, як викликати функцію, яка використовує урожай, і я використав лише один, тому що це менш багатослівний, ніж написання функції з урожайністю для такого короткого прикладу. У другому фрагменті, я використовував FunctionWithYield як generator_factory, так вона буде називатися , коли ІТЕР називається, що всякий раз , коли я пишу «для й в у».
michaelsnowden

Гарне рішення. Це фактично робить об'єкт без змін без ідентифікації, а не ітераторський об'єкт, тому сам об'єкт може бути використаний повторно. Особливо корисно, якщо ви хочете передати ітерабельний об'єкт функції, і ця функція буде використовувати об'єкт кілька разів.
Косин

5

Якщо відповідь ГжегожОледзького не буде достатньо, ви, ймовірно, можете скористатися send()для досягнення своєї мети. Дивіться PEP-0342 для отримання більш детальної інформації про покращені генератори та вираження виходу.

ОНОВЛЕННЯ: Також див itertools.tee(). Він включає деяку частину цієї пам’яті та обробку компроміту, згадану вище, але це може заощадити деяку пам’ять над просто зберіганням генераторних результатів у a 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
що з нескінченними генераторами?
роси

1
Швидкість - не єдине враження; list()зберігає все ітерабельне в пам’яті
Chris_Rands

@Chris_Rands Так буде, tee()якщо один ітератор споживає всі значення - ось як це teeпрацює.
Чемпіон AC

2
@Dewfy: для нескінченних генераторів використовуйте рішення Аарона Дігулла (функція ExpensiveSetup повертає дорогоцінні дані.)
Джефф Ліверман,

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 (), ви можете створити ітератор за допомогою цього методу, а потім виконати цикл для ітератора, який буде перебирати його значення.

Наприклад:

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) Нескінченна петля, безумовно, не те, що я хочу
роси

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

Потрібно:

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 / ОЖИДАНИЕ шлях 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те, що трохи не рекомендується для такого простого випадку.
роси
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.