如何在Python中制作重复生成器

25 投票
6 回答
18348 浏览
提问于 2025-04-15 14:03

在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时,我什么也得不到——返回的是一个空列表。

有没有简单的方法可以用yield或者生成器推导式来创建一个像xrange那样可以重复的生成器呢?我在一个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__返回一个新的迭代器)。

6 个回答

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

小提示:我没有测试性能,这样创建类可能会浪费资源。不过,真是太棒了;-)

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

撰写回答