Программирование

Почему вложенные итераторы генераторе дают первую пару?

В списковом генераторе внутренний итератор создаётся один раз и переиспользуется, поэтому после первой пары внешняя петля не получает новых значений. Узнайте почему.

Why does a list comprehension that nests two pre‑created iterators produce only the first pair of tuples instead of all combinations?

python
range1 = range(3)
range2 = range(2)
print([(e1, e2) for e1 in range1 for e2 in range2])
# → [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]

iter1 = iter(range(3))
iter2 = iter(range(2))
print([(e1, e2) for e1 in iter1 for e2 in iter2])
# → [(0, 0), (0, 1)]

The second snippet, which uses pre‑created iterator objects, unexpectedly stops after the first outer iteration. This behavior shows up on Python 2.7, 3.12, and 3.14. When the iterators are created inline, e.g. [(e1, e2) for e1 in iter(range(3)) for e2 in iter(range(2))], the expected six tuples are produced.

Is this a bug in the Python interpreter, or is there an explanation for why the outer iterator stops as soon as the inner iterator is exhausted? What is the underlying mechanism that causes this difference?

The outer iterator stops because a list comprehension creates each inner iterator only once and reuses it for every iteration of the outer loop.
When the inner iterator is exhausted after the first outer iteration, the comprehension has no more values to produce for the remaining outer values, so it returns only the first pair of tuples.

Содержание


Как работают вложенные генераторы списков?

Внутри генератора списков Python компилирует выражение в поток кода вида:

python
result = []
for e1 in range1:
    for e2 in range2:
        result.append((e1, e2))

Однако, в отличие от обычного for, итераторы (range1, range2) создаются один раз при инициализации генератора.
Сам генератор хранит ссылки на эти объекты в переменных, которые позже используются в циклах, и это ускоряет работу.
Эта особенность позволяет генератору работать быстрее, но приводит к неожиданному поведению, если один из итераторов создаётся заранее.

According to a discussion on Stack Overflow about nested for loops in list comprehensions, the outer loop uses the same inner iterator across iterations of the outer loop. (see Stack Overflow post)


Почему внутренний итератор исчерпывается

Когда вы пишете

python
iter1 = iter(range(3))
iter2 = iter(range(2))
[(e1, e2) for e1 in iter1 for e2 in iter2]

генератор получает ссылки на два уже созданных итератора.
Во время выполнения:

  1. e1 берётся из iter1 (0, затем 1, 2).
  2. Для первого значения e1 запускается внутренний цикл: e2 берётся из iter2 (0, 1) и оба элемента добавляются в список.
  3. После завершения внутреннего цикла iter2 находится в состоянии exhausted – его курсор указывает за конец последовательности.
  4. При следующей итерации внешнего цикла iter1 переменная e1 обновляется, но внутренний цикл уже не может получить новые элементы, потому что iter2 исчерпан.
    Внутренний цикл просто пропускается, и в результирующий список добавляются только пары, полученные в первой итерации внешнего цикла.

Таким образом, генератор «заперта» внутренним итератором, и дальнейшие внешние значения не приводят к новым комбинациям.

As explained in a Stack Overflow answer about missing elements when using an iterator in a list comprehension, the iterator is exhausted after the first use, so subsequent outer loop iterations have no inner values to combine with. (see Stack Overflow answer)


Сравнение с обычным циклом for

Если написать код с обычными циклами:

python
result = []
for e1 in range(3):
    for e2 in range(2):
        result.append((e1, e2))

Python каждый раз создаёт новый объект итератора для range(2).
Таким образом, даже после того как внутренний цикл завершится, при следующей итерации внешнего цикла создаётся новый iter(range(2)), и пары генерируются полностью.

В генераторе списков же:

python
[(e1, e2) for e1 in range(3) for e2 in range(2)]

объекты range(3) и range(2) создаются один раз и сохраняются, поэтому результат совпадает с обычным циклом.

The behavior of list comprehensions with inline iterators matches that of nested for loops because the iterators are recreated each time. (see DataCamp tutorial on list comprehensions)


Техническая реализация: байткод и переменные

Если посмотреть байткод генератора списков, можно увидеть, что он использует инструкции GET_ITER и FOR_ITER только один раз для каждого итератора:

python
  0 LOAD_NAME                0 (iter1)
  3 GET_ITER
  4 FOR_ITER                 1 (to 8)
  7 STORE_FAST               0 (e1)
  8 LOAD_NAME                1 (iter2)
 11 GET_ITER
 12 FOR_ITER                 1 (to 16)
 15 STORE_FAST               1 (e2)
 16 LOAD_CONST               0 ((e1, e2))
 19 BUILD_TUPLE              2
 22 LIST_APPEND              1
 25 JUMP_ABSOLUTE            4
 28 POP_BLOCK
 29 LOAD_CONST               1 (None)
 32 RETURN_VALUE

Обратите внимание, что GET_ITER для iter2 находится внутри внешнего цикла, но он вызывается только один раз: после первой итерации внешнего цикла, объект iter2 уже исчерпан, и дальнейшие вызовы FOR_ITER сразу возвращают StopIteration.
Это и приводит к тому, что тело внутреннего цикла не выполняется для оставшихся значений e1.

Bytecode inspection confirms that the inner iterator is obtained only once per comprehension. (see Python bytecode documentation)


Как обойти ситуацию

  1. Создавайте итераторы внутри генератора (как в примере с iter(range(3))).
    Это гарантирует, что каждый раз будет новый итератор.

  2. Используйте обычный for вместо генератора, если вам нужна более явная логика и контроль над созданием итераторов.

  3. Сохраняйте копии исходных итерируемых объектов и передавайте их в iter() внутри генератора, чтобы избежать повторного исчерпания.

Пример:

python
range1, range2 = range(3), range(2)
[(e1, e2) for e1 in range1 for e2 in range2]          # ✅
[(e1, e2) for e1 in iter(range1) for e2 in iter(range2)]  # ✅

Заключение

  • В генераторе списков внутренний итератор создаётся один раз и переиспользуется для всех внешних итераций.
  • После первой итерации внешнего цикла внутренний итератор исчерпывается, и дальнейшие внешние значения не дают новых комбинаций.
  • Это не баг интерпретатора, а следствие дизайна языка и оптимизации генераторов.
  • Чтобы получить ожидаемый результат, создавайте итераторы внутри генератора или используйте обычный цикл for.

Источники

  1. Stack Overflow – Nested for loop list comprehension outer loop not looping
  2. Stack Overflow – Elements missing when iterator used in a list comprehension
  3. DataCamp – Python List Comprehension Tutorial
  4. Python Dis – Bytecode documentation
Авторы
Проверено модерацией
Модерация