【问题标题】:Can Python generators be invoked non-lazily?可以非懒惰地调用 Python 生成器吗?
【发布时间】:2019-04-19 09:11:45
【问题描述】:

我知道在 Python 中,生成器是延迟调用的。例如:

>>> def G():
...     print('this was evaluated now 1')
...     yield 1
...     print('this was evaluated now 2')
...     yield 2
...
>>> g = G()
>>> next(g)
this was evaluated now 1
1
>>> next(g)
this was evaluated now 2
2

print('this was evaluated now 1') 行仅在第一个 next(g) 被调用后才被评估。

我想知道是否有一种简单的方法可以非懒惰地调用生成器。这意味着当调用g = G() 时,该函数将计算直到并包括第一个yield 结果在内的所有内容,而不会实际产生。然后,在第一次调用next(g) 时,将产生已经计算的结果,并且还将计算直到并包括第二个yield 结果的所有内容。以此类推。

如何做到这一点?


这是在这种非惰性方案下的预期行为:

>>> g = G()
this was evaluated now 1
>>> next(g)
1
this was evaluated now 2
>>> next(g)
2

这是一个解决方案尝试,但不起作用:

>>> class NonLazyGenerator():
...     def __init__(self,G):
...         self.g = G()
...         self.next_value = next(self.g)
...
...     def __next__(self):
...         current_value = self.next_value
...         try:
...             self.next_value = next(self.g)
...         except StopIteration:
...             pass
...         return current_value
...
>>> g = NonLazyGenerator(G)
this was evaluated now 1
>>> next(g)
this was evaluated now 2
1
>>> next(g)
2

这将失败,因为该值仅在 return 语句之后产生,而直到下一个 yield 的所有内容的计算发生在 return 语句之前。这个例子让我意识到它可能无法执行我正在寻找的东西,因为它需要在函数返回后执行步骤(可能需要多线程)。

【问题讨论】:

  • 你为什么想要那个? Yielding 仍然会为下一个结果做计算工作,所以你什么也得不到。
  • 我可以解释我为什么要这样做,但我的问题的目的不是要找到支持或反对这样做的理由,而是要了解如何做到这一点。
  • 原因是我有一个函数,它的主体中有一个yield 语句,这会将函数变成一个生成器。但是,这个yield 只能根据函数参数的值来访问。我希望该函数在此参数为 False 时充当常规函数,否则充当生成器。如果生成器会被非懒惰地评估。这将解决问题。 (当然,还有其他方法可以解决这个问题,当然,这也不一定是好的编程,但正如我所说,我目前只对问题的答案感兴趣)

标签: python generator


【解决方案1】:

你可以为它写一些装饰器,比如:

def eagergenerator(mygen):
    class GeneratorWrapper:
        def __init__(self, *args, **kwargs):
            self.g = mygen(*args, **kwargs)
            self.last = next(self.g)
        def __iter__(self):
            return self
        def __next__(self):
            if self.last is self:
                raise StopIteration
            fake_yield = self.last
            try:
                self.last = next(self.g)
                return fake_yield
            except StopIteration:
                self.last = self
                return fake_yield
    return GeneratorWrapper

然后你可以简单地装饰你的普通生成器:

@eagergenerator
def G():
    print("one")
    yield 1
    print("two")
    yield 2

它的工作原理如下:

>>> g = G()                               
one                                       
>>> next(g)                               
two                                       
1                                         
>>> next(g)                               
2                                         
>>> next(g)                               
Traceback (most recent call last):        
  File "<stdin>", line 1, in <module>     
  File "eagergen.py", line 10, in __next__
    raise StopIteration                   
StopIteration                             
>>>                                       

【讨论】:

  • 一个很好的例子(+1)。如果有人想基于它自己的代码,我想添加一些注释。我没有尝试,但我认为需要将“空”生成器作为try/next/except StopIteration__init__ 中的特殊情况处理。此外,StopIteration 的值(即生成器的返回值)应由包装器保留。
  • 这只是一个概念验证,但请随时编辑我的答案!
【解决方案2】:

信用:这是受到@L3viathan 回答的启发

在这个版本中,itertools.tee 用于存储包装器在原始生成器后面的一个生成值。

import itertools

def eagergenerator(mygen):
    class GeneratorWrapper:
        def __init__(self, *args, **kwargs):
            self.g0, self.g1 = itertools.tee(mygen(*args, **kwargs))
            self._next0()
        def _next0(self):
            try:
                next(self.g0)
            except StopIteration:
                pass
        def __iter__(self):
            return self
        def __next__(self):
            self._next0()
            return next(self.g1)
    return GeneratorWrapper

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-12-31
    • 2010-09-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-09
    • 2010-11-30
    • 1970-01-01
    相关资源
    最近更新 更多