`zip()` 中生成器的额外 next()?

Extra next() for generator in `zip()`?

给定,

import itertools as it
def foo():
    idx = 0
    while True:
        yield idx
        idx += 1

k = foo()

当我如下使用zip()时,

>>> list(zip(k,[11,12,13]))
[(0, 11), (1, 12), (2, 13)]

紧接着,

>>> list(zip(k,[11,12,13]))
[(4, 11), (5, 12), (6, 13)]

请注意,第二个 zip 应该以 (3,11) 开头,但它跳到了 (4,11) 反而。就好像某处隐藏着另一个next(k)。当我使用 it.islice

时不会发生这种情况
>>> k = foo()
>>> list(it.islice(k,6))
[0, 1, 2, 3, 4, 5]

注意 it.islice 没有遗漏 3 项。

我正在使用 Python 3.8.

zip 基本上(并且必然,考虑到迭代器协议的设计)是这样工作的:

 # zip is actually a class, but we'll pretend it's a generator
 # function for simplicity.
 def zip(xs, ys):
     # zip doesn't require its arguments to be iterators, just iterable
     xs = iter(xs)
     ys = iter(ys)
     while True:
         x = next(xs)
         y = next(ys)
         yield x, y

xs 的元素被消耗之前无法判断 ys 是否耗尽,并且迭代器协议没有为 zip 提供放置 x 如果 next(ys) 引发 StopIteration 异常,xs 中的“返回”。

对于其中一个输入迭代器是 sized 的特殊情况,您可以比 zip:

做得更好一点
import itertools as it
from collections.abc import Sized

def smarter_zip(*iterables):
    sized = [i for i in iterables if isinstance(i, Sized)]
    try:
        min_length = min(len(s) for s in sized)
    except ValueError:
        # can't determine a min length.. fall back to regular zip
        return zip(*iterables)
    return zip(*[it.islice(i, min_length) for i in iterables])

它使用 islice to prevent zip 从每个迭代器中消耗比我们知道的更多是绝对必要的。这smarter_zip将解决原始问题中提出的案例的问题。

然而,在一般情况下,没有办法事先判断迭代器是否耗尽(考虑一个生成器产生到达套接字的字节)。如果最短的 iterables 没有大小,原来的问题仍然存在。为了解决一般情况,您可能希望将迭代器包装在一个 class 中,它会记住最后生成的项目,以便在必要时可以从内存中调用它。