遍历生成器序列

5
我有一系列生成器:(gen_0,gen_1,... gen_n)。
这些生成器将懒惰地创建它们的值,但是它们都是有限的,并且可能长度不同。
我需要能够构造另一个生成器,按顺序产生每个生成器的第一个元素,然后是第二个元素等,跳过已经耗尽的生成器的值。
我认为这个问题类似于取元组。
((1, 4, 7, 10, 13, 16), (2, 5, 8, 11, 14), (3, 6, 9, 12, 15, 17, 18))

我正在解决一个简单的问题,需要使用(genA、genB、genC)。其中,genA按顺序产生数值(1, 4, 7, 10, 13, 16),genB产生数值(2, 5, 8, 11, 14),genC产生数值(3, 6, 9, 12, 15, 17, 18)。需要遍历它们,使得它们能够按顺序产生1到18的数字。

如果元组的元素长度相同,解决由元组元组构成的简单问题非常简单。如果变量'a'指向了这个元组,你可以使用以下代码:

[i for t in zip(*a) for i in t]

很不幸,这些项的长度不一定相同,而且使用zip技巧在生成器上似乎无法工作。 到目前为止,我的代码非常丑陋,我无法找到任何接近简洁解决方案的东西。有帮助吗?

itertools.izip_longest;你可以传递一个标记来填充用尽的生成器。如果需要,你可以过滤掉结果中的标记。 - Katriel
4个回答

8

我认为你需要使用 itertools.izip_longest 方法。

>>> list([e for e in t if  e is not None] for t in itertools.izip_longest(*some_gen,
                                                               fillvalue=None))
[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12], [13, 14, 15], [16, 17], [18]]
>>> 

2
我建议使用 "if e is not None"。 - kaspersky
2
另外,如果“None”是一个有效的值呢? - kaspersky
处理None的情况并保持这种方法很容易。只需在前面添加一行sentinel = object(),然后使用e is not sentinelfillvalue=sentinel即可。 - DSM
啊,我刚刚注意到@katriealex比我早很多小时提出了那个建议。我会留下评论,但承认它是迟到的。 - DSM

4
如果你查看itertools.izip_longest的文档,你会发现它提供了一个纯Python实现。很容易修改这个实现,使其产生你需要的结果(即像izip_longest一样,但没有任何fillvalue)。
class ZipExhausted(Exception):
    pass

def izip_longest_nofill(*args):
    """
    Return a generator whose .next() method returns a tuple where the
    i-th element comes from the i-th iterable argument that has not
    yet been exhausted. The .next() method continues until all
    iterables in the argument sequence have been exhausted and then it
    raises StopIteration.

    >>> list(izip_longest_nofill(*[xrange(i,2*i) for i in 2,3,5]))
    [(2, 3, 5), (3, 4, 6), (5, 7), (8,), (9,)]
    """
    iterators = map(iter, args)
    def zip_next():
        i = 0
        while i < len(iterators):
            try:
                yield next(iterators[i])
                i += 1
            except StopIteration:
                del iterators[i]
        if i == 0:
            raise ZipExhausted
    try:
        while iterators:
            yield tuple(zip_next())
    except ZipExhausted:
        pass

这样可以避免重新过滤izip_longest的输出来丢弃填充值。或者,如果您想要一个“扁平化”的输出:

def iter_round_robin(*args):
    """
    Return a generator whose .next() method cycles round the iterable
    arguments in turn (ignoring ones that have been exhausted). The
    .next() method continues until all iterables in the argument
    sequence have been exhausted and then it raises StopIteration.

    >>> list(iter_round_robin(*[xrange(i) for i in 2,3,5]))
    [0, 0, 0, 1, 1, 1, 2, 2, 3, 4]
    """
    iterators = map(iter, args)
    while iterators:
        i = 0
        while i < len(iterators):
            try:
                yield next(iterators[i])
                i += 1
            except StopIteration:
                del iterators[i]

2

如果您希望将它们全部折叠成单个列表,可以使用另一个itertools选项;正如@gg.kaspersky在另一个线程中指出的那样,这种方法无法处理生成的None值。

g = (generator1, generator2, generator3)

res = [e for e in itertools.chain(*itertools.izip_longest(*g)) if e is not None]
print res

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]

1
你可能考虑使用itertools.izip_longest,但如果None是一个有效值,那么这个解决方案将失败。这里有一个示例的“另一个生成器”,它恰好做到了你所要求的,并且非常简洁:
def my_gen(generators):
    while True:
        rez = () 
        for gen in generators:
            try:
                rez = rez + (gen.next(),)
            except StopIteration:
                pass
        if rez:
            yield rez
        else:
            break

print [x for x in my_gen((iter(xrange(2)), iter(xrange(3)), iter(xrange(1))))]

[(0, 0, 0), (1, 1), (2,)] #output

你可以使用 iter(range(x)) 替代 simple_gen(x) - Gareth Rees
或者更好的方法是在Python 2.x中使用iter(xrange(x)) - phant0m
感谢所有的回答。最后一个回答完全符合我的要求,并且我可以理解。但是,为了代码的性能,我将使用基于itertools.izip_longest的解决方案。这又是我阅读文档却没有看到可能用途的情况... - Sean Holdsworth

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