如何在Python中制作一个可重复使用的生成器

25
如何在Python中创建一个像xrange一样的重复生成器?例如,如果我执行以下操作:
>>> m = xrange(5)
>>> print list(m)
>>> print list(m)

我两次都得到了相同的结果——数字0..4。然而,如果我尝试使用yield:
>>> def myxrange(n):
...   i = 0
...   while i < n:
...     yield i
...     i += 1
>>> m = myxrange(5)
>>> print list(m)
>>> print list(m)

第二次尝试迭代m时,我什么也没有得到 - 一个空列表。
是否有一种简单的方法创建像xrange一样的重复生成器,使用yield或生成器理解?我在Python跟踪问题上找到了一个解决方法,它使用装饰器将生成器转换为迭代器。这会在每次开始使用它时重新启动,即使上次没有使用所有值,就像xrange一样。我还想出了自己的装饰器,基于相同的想法,它实际上返回一个生成器,但可以在抛出StopIteration异常后重新启动:
@decorator.decorator
def eternal(genfunc, *args, **kwargs):
  class _iterable:
    iter = None
    def __iter__(self): return self
    def next(self, *nargs, **nkwargs):
      self.iter = self.iter or genfunc(*args, **kwargs):
      try:
        return self.iter.next(*nargs, **nkwargs)
      except StopIteration:
        self.iter = None
        raise
  return _iterable()

有没有更好的方法来解决这个问题,只使用yield和/或生成器推导?或者Python内置了什么东西?所以我不需要自己编写类和装饰器?
更新 u0b34a0f6ae的评论准确指出了我的误解的根源:
xrange(5)并不返回迭代器,它创建了一个xrange对象。xrange对象可以像字典一样被迭代多次。
我的“永恒”函数完全走错了路,表现得像一个迭代器/生成器(__iter__返回self),而不是像一个集合/xrange(__iter__返回一个新的迭代器)。

2
小小的挑剔,但 xrange() 不是一个生成器。type(xrange(4)) != type(myxrange(4)) - John Millikin
2
我认为那不仅是小问题。那就是差异的全部原因。正如John所指出的,可以通过重载__iter__来获得所需的行为。 - ricree
我在跟进你的实现代码方面遇到了很多困难,与另外两个提议的实现(一个在你链接的Python跟踪问题中,另一个在@JohnMillikin的答案中)相比。特别是,我很难弄清楚:(1)"@decorator.decorator" 到底意味着什么。你能给出它的文档链接吗?(2)一个使用示例会非常有帮助;特别是,一个涉及args和nargs的示例。(3)你能举个例子说明你的StopIteration处理如何增加价值吗?即一个你的实现成功而其他两个实现失败的例子。 - Don Hatch
@DonHatch 希望我刚刚在问题中添加的更新能够解释为什么我的实现代码难以理解(而且明显是错误的)。 - Alice Purcell
6个回答

21

不能直接实现。生成器能够用于协程、资源管理等的一部分灵活性在于它们始终是一次性的。一旦运行,生成器就无法重新运行。您必须创建一个新的生成器对象。

但是,您可以创建自己的类来覆盖__iter__()。它将像可重复使用的生成器一样运作:

def multigen(gen_func):
    class _multigen(object):
        def __init__(self, *args, **kwargs):
            self.__args = args
            self.__kwargs = kwargs
        def __iter__(self):
            return gen_func(*self.__args, **self.__kwargs)
    return _multigen

@multigen
def myxrange(n):
   i = 0
   while i < n:
     yield i
     i += 1
m = myxrange(5)
print list(m)
print list(m)

这基本上与我在问题中提供的解决方法完全相同 - 我猜你错过了。但还是谢谢! - Alice Purcell
2
“Workaround”。这并不复杂。xrange(5)不会返回一个迭代器,而是创建了一个xrange对象。xrange对象可以像字典一样被多次迭代。 - u0b34a0f6ae

14

使用itertools非常容易。

import itertools

alist = [1,2,3]
repeatingGenerator = itertools.cycle(alist)

print(next(generatorInstance)) #=> yields 1
print(next(generatorInstance)) #=> yields 2
print(next(generatorInstance)) #=> yields 3
print(next(generatorInstance)) #=> yields 1 again!

2
这会导致print list(m)(问题要求中的一个要求)永远运行,但是 :/ - Alice Purcell

2
如果你需要写很多这样的代码,John Millikin 的回答是最干净的。
但如果你不介意增加 3 行和一些缩进,你可以不使用自定义装饰器来完成。这包含了两个技巧:
1. [通用技巧:] 你可以很容易地使一个类可迭代,而无需实现 `.next()` - 只需在 `__iter__(self)` 中使用生成器即可!
2. 不必费心去构造一个类,你可以在函数内部定义一个一次性的类。
=>
def myxrange(n):
    class Iterable(object):
        def __iter__(self):
            i = 0
            while i < n:
                yield i
                i += 1
    return Iterable()

小字提示:我没有测试性能,像这样生成类可能会浪费资源。但很棒;-)


0
您可以使用第三方工具more_itertools.seekable重置迭代器。
通过> pip install more_itertools进行安装。
import more_itertools as mit


def myxrange(n):
    """Yield integers."""
    i = 0
    while i < n:
        yield i
        i += 1

m = mit.seekable(myxrange(5))
print(list(m))
m.seek(0)                                              # reset iterator
print(list(m))
# [0, 1, 2, 3, 4]
# [0, 1, 2, 3, 4]

注意:在迭代器前进时,内存消耗会增加,因此请小心处理大型可迭代对象。

0

我认为答案是“不行”。我可能错了。在2.6中,使用生成器进行参数和异常处理的一些新功能可能允许您实现类似于您想要的内容。但是这些功能大多用于实现半连续性。

为什么您不想拥有自己的类或装饰器?为什么您想创建一个返回生成器而不是类实例的装饰器?


(1) 因为需要更多的代码,而且似乎可能已经以我不知道的某种方式实现了。 (2) 因为我最初误解了xrange,认为我想要一个永久的生成器而不是一个可迭代对象。 - Alice Purcell

-1
请使用以下解决方案:
>>> myxrange_ = lambda x: myxrange(x)
>>> print list(myxrange_(5))
... [0, 1, 2, 3, 4]
>>> print list(myxrange_(5))
... [0, 1, 2, 3, 4]

>>> for number in myxrange_(5):
...     print number
... 
    0
    1
    2
    3
    4
>>>

并且使用装饰器:

>>> def decorator(generator):
...     return lambda x: generator(x)
...
>>> @decorator
>>> def myxrange(n):
...   i = 0
...   while i < n:
...     yield i
...     i += 1
...
>>> print list(myxrange(5))
... [0, 1, 2, 3, 4]
>>> print list(myxrange(5))
... [0, 1, 2, 3, 4]
>>>

简单。


无法理解¿-1?这个解决方案可以满足问题的要求。 - SmartElectron
不,它不会,因为重复迭代所需的参数没有封装在对象中 - 每次需要新的重用时都必须传递它们。生成器对象的整个目的是封装执行迭代所需的信息。可重用的生成器也应该这样做。请注意,John Millikin的解决方案不要求您每次使用生成器时都输入“5”。您只需输入“5”一次,对象就会从那时起记住它。 - Paul
此外,语法与生成器不同。需要额外的括号。 - Paul

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接