如何在Python中制作重复生成器
在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 个回答
如果你写很多这样的代码,John Millikin的回答是最简洁的。
不过,如果你不介意多加3行代码和一些缩进的话,其实可以不使用自定义装饰器。这个方法结合了两个小技巧:
[一般有用:] 你可以很简单地让一个类变得可迭代,而不需要实现
.next()
方法——只需在__iter__(self)
中使用生成器就可以了!你可以在一个函数内部定义一个临时类,而不需要麻烦地写构造函数。
=>
def myxrange(n):
class Iterable(object):
def __iter__(self):
i = 0
while i < n:
yield i
i += 1
return Iterable()
小提示:我没有测试性能,这样创建类可能会浪费资源。不过,真是太棒了;-)
用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!
不可以直接这样做。生成器的灵活性之一是它们只能使用一次。也就是说,一旦运行过,生成器就不能再运行了。如果你想再用,就得创建一个新的生成器对象。
不过,你可以自己创建一个类,重写 __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)