首页 > 解决方案 > 如何改进这个交替迭代器的 Python 3 代码?

问题描述

我最近编写了这个 Python 3 代码,它应该在给它的所有迭代中交替。也就是说,如果函数作为参数给出,(first, second, third)那么它会产生first[0], second[0], third[0], first[1], .... 如果second在其他人之前用完,则跳过:second[15], third[16], first[16], third[16], ...直到所有可迭代项都用完。

这里是。它是功能性的,但它看起来不是很“pythonic”。我特别不喜欢保留一组标志来告诉我生成器是否为空。

def zipper(*many):
    iterators = [iter(x) for x in many]
    iHasItems = [True]*len(iterators)
    while any(iHasItems):
        for n, iterator in enumerate(iterators):
            if iHasItems[n]:
                try:
                    yield next(iterator)
                except StopIteration:
                    iHasItems[n] = False

标签: pythonrefactoring

解决方案


您基本上是在重新实现roundrobin()itertools 文档配方部分中记录的功能:

from itertools import cycle, islice

def roundrobin(*iterables):
    "roundrobin('ABC', 'D', 'EF') --> A D E B F C"
    # Recipe credited to George Sakkis
    num_active = len(iterables)
    nexts = cycle(iter(it).__next__ for it in iterables)
    while num_active:
        try:
            for next in nexts:
                yield next()
        except StopIteration:
            # Remove the iterator we just exhausted from the cycle.
            num_active -= 1
            nexts = cycle(islice(nexts, num_active))

这会循环遍历迭代器并在每次StopIteration引发异常时切掉最后一个;最后一个迭代器总是那个刚刚用完的迭代器。

具体来说,对于输入示例,以、 在这些位置和的nexts循环列表开始,然后算法继续执行:<iter('ABC'), iter('D'), iter('EF')>num_active3

  1. 屈服A与离开<iter('D'), iter('EF'), iter('BC')>
  2. 屈服D与离开<iter('EF'), iter('BC'), <iter('')>
  3. 屈服E与离开<iter('BC'), <iter(''), iter('F')>
  4. 屈服B与离开<iter(''), iter('F'), iter('C')>
  5. 试图让步但遇到StopIteration异常;然后循环在<iter('F'), iter('C'), iter(*stopped*)>,所以num_active变成2cycle(islice(nexts, 2))将循环设置为<iter('F'), iter('C')>并且while循环继续到
  6. 屈服F与离开<iter('C'), iter('')>
  7. 屈服C与离开<iter(''), iter('')>

之后最后两个空迭代器触发进一步的StopIteration异常,并num_active从 2 到 1 到 0 并且while循环结束。

您可以使用collections.deque()对象和手动旋转来实现相同的功能:

from collections import deque

def roundrobin(*iterables):
    nexts = deque((iter(it).__next__ for it in iterables))
    while nexts:
        try:
            yield nexts[0]()
            # rotate the queue to the left for the next step
            nexts.rotate(-1)
        except StopIteration:
            # Remove the iterator we just exhausted from the queue
            nexts.popleft()

但是这种方法比cycle变体慢,因为旋转是“手动”完成的,每次迭代都会产生成本,超过了更简单的“用尽”异常情况实现。

与您的方法一样,这使您不必反复尝试迭代任何已经用尽的迭代器,并且与zip_longest()其他人发布的方法不同,它不需要您在每次迭代时测试哨兵值(item is not Nonenot item或)。item is not unique_sentinel_singleton


推荐阅读