Почему zip() удаляет значения моего генератора?

Я писал ответ на этот вопрос, когда заметил, что моя простая реализация не дала правильных результатов. При поиске ошибки я заметил следующее:

In [1]: import itertools
In [2]: gen = itertools.cycle((0,1,2))

In [3]: zip(gen, range(3))
Out[3]: [(0, 0), (1, 1), (2, 2)]

In [4]: zip(gen, range(3))
Out[4]: [(1, 0), (2, 1), (0, 2)]

По какой-либо причине метод gen next() называется одним дополнительным временем. Чтобы проиллюстрировать это, я использовал следующее:

class loudCycle(itertools.cycle):
    def next(self):
        n = super(loudCycle, self).next()
        print n
        return n

In [6]: gen = loudCycle((0,1,2))
In [7]: zip(gen, range(3))
0
1
2
0
Out[7]: [(0, 0), (1, 1), (2, 2)]

Ответ 1

Это происходит потому, что zip оценивает итераторы слева направо, а это означает, что после трех шагов он вызывает next() на gen и только затем на iter(range(3)) (или что-то в этом роде) и встречается с StopIteration. Чтобы обойти это, используйте более короткий (конечный) итерабельный как самый левый аргумент:

In [8]: zip(range(3), gen)
0
1
2
Out[8]: [(0, 0), (1, 1), (2, 2)]

Ответ 2

Ваш собственный ответ является правильным, и представляет собой очень хорошее решение - если один из аргументов zip всегда короче другого. Однако в ситуациях, когда вы не знаете, что будет короче, вы можете найти islice полезным. islice также обеспечивает легкое обходное решение, если вы хотите, чтобы первый элемент в ваших кортежах был из вашего генератора. В вашем случае вы можете сделать это:

>>> import itertools
>>> gen = itertools.cycle(('a', 'b', 'c'))
>>> seq = range(3)
>>> zip(itertools.islice(gen, len(seq)), seq)
[('a', 0), ('b', 1), ('c', 2)]
>>> zip(itertools.islice(gen, len(seq)), seq)
[('a', 0), ('b', 1), ('c', 2)]

Ваш ответ, вероятно, лучше в этом случае - это, конечно, проще - но я думал, что добавлю это как дополнение.