Python,使用yield来实现循环生成器

时间:2017-12-12 12:22:17

标签: python iterator generator

TL; DR是我试图为基于收益的发电机做得太复杂了吗?

我有一个python应用程序,我需要在一个对象列表上重复一次昂贵的测试,一次一个,然后修改那些通过的对象。我希望有几个对象可以传递,但是我不想创建所有传递的对象的列表,因为mangle会改变其他一些对象的状态。无需按任何特定顺序进行测试。然后冲洗并重复直至某些停止状态。

我的第一个简单实现就是这个,它在逻辑上正确运行

while not stop_condition:
    for object in object_list:
        if test(object):
            mangle(object)
            break
    else:
        handle_no_tests_passed()
遗憾的是,for object in object_list:始终在列表的开头重新启动,其中对象可能尚未更改,并且列表末尾的对象已准备好进行测试。随机挑选它们会稍微好一点,但我宁愿继续前一段时间从前一段时间开始。我仍然希望for / in调用在它遍历整个列表时终止。

这听起来像是一份收益的工作,但是我把我的大脑绑在一起,没能让它按照我的意愿行事。我可以在简单的情况下使用它,迭代一个范围或从某些来源返回过滤的记录,但我无法找到如何使其保存状态并从源头重新开始读取。

我经常可以用类很长的罗嗦的方式做事,但却不明白如何使用像yield这样的简化。这是一个完全符合我想要的解决方案。

class CyclicSource:
    def __init__(self, source):
        self.source = source
        self.pointer = 0

    def __iter__(self):
        # reset how many we've done, but not where we are
        self.done_this_call = 0
        return self

    def __next__(self):
        ret_val = self.source[self.pointer]
        if self.done_this_call >= len(self.source):
            raise StopIteration
        self.done_this_call += 1
        self.pointer += 1
        self.pointer %= len(self.source)
        return ret_val

source = list(range(5))
q = CyclicSource(source)

print('calling once, aborted early')
count = 0
for i in q:
    count += 1
    print(i)
    if count>=2:
        break
else:
    print('ran off first for/in')

print('calling again')
for i in q:
    print(i)
else:
    print('ran off second for/in')

演示了所需的行为

calling once, aborted early
0
1
calling again
2
3
4
0
1
ran off second for/in

最后,问题。是否可以使用yield来简化生成器语法来执行我想要的操作,或者在连续的for / in调用之间保持状态是否需要完整的类语法?

1 个答案:

答案 0 :(得分:1)

使用__iter__方法会导致重置迭代器。这实际上与迭代器的常规行为相反; __iter__ method should just return self, nothing more。每次创建for循环时,您都依赖于iter()for i in q:应用于.reset()副作用。这使得你的迭代器工作,但行为是令人惊讶并将绊倒未来的维护者。我希望将该效果拆分为单独的def cyclic_source(source): pointer = 0 done_this_call = 0 while done_this_call < len(source): ret_val = source[pointer] done_this_call += 1 pointer = (pointer + 1) % len(source) reset = yield ret_val if reset is not None: done_this_call = 0 yield # pause again for next iteration sequence 方法,例如。

也可以重置一个生成器,使用generator.send()发信号通知它重置:

q = cyclic_source(source)
for count, i in enumerate(q):
    print(i)
    if count == 1:
        break
else:
    print('ran off first for/in')

print('explicitly resetting the generator')
q.send(True)
for i in q:
    print(i)
else:
    print('ran off second for/in')

现在您可以将您的点数重置为零:

from itertools import cycle, islice

q = cycle(source)
for count, i in enumerate(islice(q, len(source))):
    print(i)
    if count == 1:
        break
else:
    print('ran off first for/in')

for i in islice(q, len(source)):
    print(i)
else:
    print('ran off second for/in')
然而,这是与可读性相反的。我会使用itertools.cycle()来使用无限生成器,但itertools.islice()的迭代次数有限:

q

source将在无限循环中从islice()生成值。 len(source)q元素之后切断迭代。但由于__iter__ 重用,它仍然保持迭代状态。

如果你必须有一个专用的迭代器,坚持一个类对象并创建一个 iterable ,那么每次调用from itertools import cycle, islice class CyclicSource: def __init__(self, source): self.length = len(source) self.source = cycle(source) def __iter__(self): return islice(self.source, self.length) 时它都会返回一个新的迭代器:

cycle()

这使状态保持在islice()迭代器中,但每次为此创建迭代器时,只需创建一个新的islice()对象。它基本上封装了上面的{{1}}方法。