Генераторы, замыкания и неожиданный вывод в Python

Генераторы, замыкания и неожиданный вывод в Python

@python_quiz

Разберем этот квиз

Кратко: небольшой фрагмент с генераторными выражениями выглядит простым, но из-за замыкания переменной цикла и ленивой оценки генераторов результат может удивить. Разберём код, почему он выдаёт такой результат и как получить ожидаемое поведение.

Исходный код

python

Что делает этот код:

  • Создаётся список из трёх генераторных выражений.
  • Вызывается next для первого генератора (это потребляет первый элемент).
  • Затем каждый генератор приводится к списку для печати.

Почему результат не тот, что кажется на первый взгляд

Ключевые моменты:

  1. Генераторное выражение (i * x for x in range(3)) замыкает переменную i по ссылке, а не по значению.
  2. После выполнения внешнего list-comprehension цикл for i in range(3) завершён, и значение переменной i равно 2 (последнее значение в range(3)).
  3. Генераторы вычисляются лениво — выражение i * x вычисляется только при фактической итерации по каждому генератору, и тогда i имеет текущее (внешнее) значение.
  4. Поэтому при итерации все три генератора будут использовать i == 2 и выдадут числа 0, 2, 4.
  5. Однако вызов next(gens[0]) заранее снимет с первого генератора первый элемент (0), так что при последующей конвертации в список первый генератор даст [2, 4], а остальные — [0, 2, 4].

Итоговый вывод программы:

text

Анализ вариантов (почему остальные неверны)

  • Вариант с тремя одинаковыми списками [0,2,4] соответствует поведению генераторов без учёта вызова next — но next съедает первый элемент первого генератора, так что он уже не может быть полным.
  • Вариант с [[0,0,0],[1,1,1],[2,2,2]] предполагает, что внутри итерации x фиксирован и умножается на разные i как будто при создании генератора значение i захватывается по значению — это неверно для замыканий в таких генераторных выражениях.
  • Вариант с одним вложенным списком явно не соответствует структуре кода (у нас три генератора).

Демонстрация: запуск и вывод

Вот воспроизведение с выводом в Python:

python

Ожидаемый вывод:

text

(Первый print показывает, что при полном обходе все три генератора дают 0,2,4 — потому что i==2 при итерации. Второй — учитывает предварительно вызванный next для первого генератора.)

Как получить "интуитивно ожидаемое" поведение

Если нужно, чтобы каждый генератор «запомнил» своё значение i при создании, можно привязать i к локальной переменной (замыкать по значению). Несколько способов:

1) Использовать фабрику через лямбда с параметром по умолчанию:

python

2) Создать списки сразу (если ленивость не нужна):

python

3) Использовать явную вложенную функцию-фабрику:

python

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

Выводы

  • Генераторные выражения замыкают переменные по ссылке; значение свободных переменных берётся при итерации, а не при создании генератора.
  • Ленивость генераторов и побочные операции (как next) влияют на содержимое при последующих проходах.
  • Чтобы зафиксировать текущее значение внешней переменной при создании генератора, нужно явно привязать её к локальной переменной (через аргументы по умолчанию, фабрику и т.п.) или отказаться от ленивости и создать список сразу.

Report Page