簡體   English   中英

重置發電機 object Python

[英]Resetting generator object in Python

我有一個生成器 object 由 multiple yield 返回。 調用此生成器的准備工作是相當耗時的操作。 這就是為什么我想多次重復使用生成器。

y = FunctionWithYield()
for x in y: print(x)
#here must be something to reset 'y'
for x in y: 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 密集型的,你最終會計算兩次。 另一方面, 2的缺點是存儲。 整個值列表將存儲在內存中。 如果值太多,那可能是不切實際的。

所以你有經典的內存與處理權衡 我無法想象在不存儲值或再次計算它們的情況下重繞生成器的方法。

另一種選擇是使用itertools.tee()函數來創建生成器的第二個版本:

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

如果原始迭代可能無法處理所有項目,則從內存使用的角度來看,這可能是有益的。

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

這樣,您可以緩存昂貴的計算。

如果您可以同時將所有結果保存在 RAM 中,那么使用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

使用包裝函數來處理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

來自tee 的官方文檔

通常,如果一個迭代器在另一個迭代器啟動之前使用了大部分或全部數據,則使用 list() 而不是 tee() 會更快。

所以最好在你的情況下使用list(iterable)

您可以定義一個返回生成器的函數

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)

我不確定你說的昂貴的准備是什么意思,但我想你實際上有

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

沒有重置迭代器的選項。 Iterator 通常在遍歷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>)轉換為迭代器

您現在可以使用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)

注意:隨着迭代器的推進,內存消耗會增加,所以要警惕大的迭代器。

您可以通過使用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).

它是如何為我工作的。

csv_rows = my_generator()
for _ in range(10):
    for row in csv_rows:
        print(row)
    csv_rows = my_generator()

好吧,你說你想多次調用一個生成器,但是初始化很昂貴......這樣的事情怎么樣?

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

我的答案解決了稍微不同的問題:如果生成器的初始化成本很高,並且每個生成的對象的生成成本都很高。 但是我們需要在多個函數中多次消耗生成器。 為了只調用生成器和每個生成的對象一次,我們可以使用線程並在不同的線程中運行每個消費方法。 由於 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())

如果你想多次重用這個生成器,你可以使用functools.partial

from functools import partial
func_with_yield = partial(FunctionWithYield)

for i in range(100):
    for x in func_with_yield():
        print(x)

這會將生成器 function 包裝在另一個 function 中,因此每次調用func_with_yield()時,它都會創建相同的生成器 function。

注意:如果您有 arguments,它也接受 function arguments partial(FunctionWithYield, args)

它可以通過代碼對象來完成。 這是示例。

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM