【问题标题】:Resetting generator object in Python在 Python 中重置生成器对象
【发布时间】:2010-11-19 06:29:00
【问题描述】:

我有一个由多个 yield 返回的生成器对象。准备调用这个生成器是相当耗时的操作。这就是为什么我想多次重复使用生成器。

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

当然,我正在考虑将内容复制到简单列表中。有没有办法重置我的发电机?

【问题讨论】:

    标签: python generator yield


    【解决方案1】:

    发电机不能倒带。您有以下选择:

    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: 确定: def call_my_func(): return FunctionWithYield(param1, param2)
    • @Dewfy “保存函数调用的签名”是什么意思?你能解释一下吗?你的意思是保存传递给生成器的参数吗?
    • (1) 的另一个缺点是 FunctionWithYield() 不仅代价高昂,而且不可能重新计算,例如如果它是从标准输入读取的。
    • 为了呼应@Max 所说的,如果函数的输出可能(或将会)在调用之间发生变化,(1)可能会产生意外和/或不希望的结果。
    【解决方案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)
    

    如果原始迭代可能无法处理所有项目,则从内存使用的角度来看,这可能是有益的。

    【讨论】:

    • 如果你想知道在这种情况下它会做什么,它本质上是缓存列表中的元素。因此,您不妨使用y = list(y),而其余代码不变。
    • 看看 implmentation(docs.python.org/library/itertools.html#itertools.tee) - 这使用延迟加载策略,所以列表项仅按需复制
    • @Dewfy:这会更慢,因为无论如何都必须复制所有项目。
    • 是的,在这种情况下 list() 更好。 tee 仅在您不使用整个列表时才有用
    • tee() 不是我的 T 恤。为什么不把y转成函数:y = lambda: FunctionWithYield(),然后for x in y():
    【解决方案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
    

    【讨论】:

    • 它有 2 个缺点:1) 在 StopIteration 之前你不能耗尽;2) 它不适合任何生成器(例如范围)
    【解决方案4】:

    可能最简单的解决方案是将昂贵的部分包装在一个对象中并将其传递给生成器:

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

    这样,您可以缓存昂贵的计算。

    如果您可以同时将所有结果保存在 RAM 中,则使用 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)
      

      【讨论】:

      • @Dewfy 在第一个 sn-p 中,生成器位于“squares = ...”行。生成器表达式的行为方式与调用使用 yield 的函数的方式相同,我只使用了一个,因为对于这样一个简短的示例,它比编写使用 yield 的函数更简洁。在第二个 sn-p 中,我使用 FunctionWithYield 作为 generator_factory,因此每当调用 iter 时都会调用它,即每当我写“for x in y”时。
      • 很好的解决方案。这实际上使无状态的可迭代对象而不是有状态的迭代器对象,因此对象本身是可重用的。如果您想将可迭代对象传递给函数并且该函数将多次使用该对象,则特别有用。
      【解决方案6】:

      如果 GrzegorzOledzki 的回答不够,您可以使用 send() 来实现您的目标。有关增强的生成器和 yield 表达式的更多详细信息,请参阅 PEP-0342

      更新:另见itertools.tee()。它涉及上面提到的一些内存与处理权衡,但它可能比仅将生成器结果存储在list 中节省一些内存;这取决于你如何使用生成器。

      【讨论】:

        【解决方案7】:

        如果您的生成器在某种意义上是纯粹的,它的输出仅取决于传递的参数和步数,并且您希望生成的生成器可以重新启动,那么这里有一个排序 sn-p 可能会很方便:

        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
        

        【讨论】:

          【解决方案8】:

          使用包装函数处理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
          

          【讨论】:

            【解决方案9】:

            来自official documentation of tee

            一般来说,如果一个迭代器使用了之前的大部分或全部数据 另一个迭代器启动,使用 list() 而不是 tee() 更快。

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

            【讨论】:

            • 无限生成器呢?
            • 速度不是唯一的考虑; list() 将整个迭代放入内存
            • @Chris_Rands 如果一个迭代器消耗所有值,tee() 也会如此 - 这就是 tee 的工作原理。
            • @Dewfy:对于无限生成器,使用 Aaron Digulla 的解决方案(ExpensiveSetup 函数返回宝贵的数据。)
            【解决方案10】:

            你可以定义一个返回你的生成器的函数

            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)
            

            【讨论】:

            • 谢谢你的回答,但主要问题是避免 creation ,调用内部函数只是隐藏创建 - 你创建了两次
            【解决方案11】:

            我不确定你所说的昂贵的准备是什么意思,但我猜你确实有

            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

            【讨论】:

              【解决方案12】:

              没有重置迭代器的选项。迭代器通常在遍历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(&lt;list-object&gt;)可以将列表转换为迭代器

              【讨论】:

                【解决方案13】:

                您现在可以使用more_itertools.seekable(第三方工具),它可以重置迭代器。

                通过&gt; 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)
                

                注意:内存消耗会随着迭代器的推进而增长,因此要警惕大型迭代器。

                【讨论】:

                  【解决方案14】:

                  您可以使用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) 无限循环绝对不是我想要的
                  【解决方案15】:

                  它对我有什么作用。

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

                  【讨论】:

                    【解决方案16】:

                    好吧,你说你想多次调用一个生成器,但是初始化很昂贵……这样的东西呢?

                    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__ 中重置
                    • 添加了第二个示例以响应您的评论。这本质上是一个带有重置方法的自定义生成器。
                    【解决方案17】:

                    我的回答解决了稍微不同的问题:如果生成器的初始化成本很高,并且每个生成的对象的生成成本很高。但是我们需要在多个函数中多次使用生成器。为了准确地调用生成器和每个生成的对象,我们可以使用线程并在不同的线程中运行每个使用方法。由于 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,这篇文章允许你以异步/等待方式做到这一点stackoverflow.com/a/42379188/149818
                    • 没有。 islice “创建一个迭代器,从可迭代对象中返回选定元素。跳过可迭代对象中的元素,直到到达开始。之后,元素将连续返回,除非步长设置为高于导致跳过项目的步骤。......”我的目标就是在不同的函数中多次消费每个元素,这些函数旨在消费整个迭代器,不会多次生成每个元素,也不会多次迭代迭代器。
                    【解决方案18】:

                    可以通过代码对象来完成。这是一个例子。

                    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

                    【讨论】:

                    • 好吧,实际上需要重置生成器以避免两次执行初始化代码。您的方法(1)无论如何都会执行两次初始化,(2)它涉及exec,对于这种简单的情况稍微不推荐。
                    猜你喜欢
                    • 2020-03-09
                    • 1970-01-01
                    • 2012-09-10
                    • 2013-01-05
                    • 2015-03-03
                    • 1970-01-01
                    • 2017-06-26
                    • 2010-10-31
                    相关资源
                    最近更新 更多