сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Асинхронность в Python

Генераторы и итераторы в Python

Зачем это спрашивают

Ленивая обработка - фирменная сила Python: гигабайтный файл строка за строкой в O(1) памяти. Вопрос-детектор практики: «почему второй проход по генератору ничего не вернул» - на нём горят регулярно.

// Понимание «итератор одноразов» экономит часы отладки «куда делись данные».

Протокол итерации: что крутит for

Итерируемое отдаёт итератор через iter(); итератор выдаёт элементы через next() и бросает StopIteration в конце. Цикл for - просто сахар вокруг этого протокола.

Ключевое различие: список можно обходить сколько угодно раз (каждый for берёт свежий итератор), а сам итератор одноразов - пройден и пуст.

// Файл - сам итератор по строкам: for line in f - идиома; f.read() ради построчной обработки - загрузка всего файла в память зря.

StopIteration
сигнал конца итератора; for ловит его сам

Генераторы: код, замороженный между yield

Генератор - функция с yield: вызов не исполняет тело, а возвращает объект-итератор. Код бежит до первого yield только при первом next(); между шагами всё состояние замораживается.

Ленивость это O(1) память: конвейер генераторов прокачивает данные поштучно, sum(x*x for x in xs) не строит промежуточный список.

// Выбор скобок: () - генераторное выражение, лениво и одноразово; [] - list comprehension, сразу и в памяти. Критерий - нужен ли повторный доступ.

yield from
делегирование вложенному итерируемому изнутри генератора

itertools: конвейерная сборка

itertools - детали конвейера: chain склеивает потоки, islice режет без материализации, product заменяет вложенные циклы, tee размножает итератор (с буфером).

groupby группирует только смежные элементы - без предварительной сортировки по тому же ключу группы окажутся порваны на куски. Это ловушка №1 модуля.

// return внутри генератора завершает его: значение уедет в StopIteration, а не наружу. Для «вернуть и продолжить» есть только yield.

groupby
группирует смежное; требует сортировки по ключу группировки

Как отвечать: «Почему второй проход по генератору ничего не вернул?»

Потому что генератор - одноразовый итератор: элементы выданы, внутри StopIteration, и повторный обход молча даёт пустоту - без ошибки. Классика: посчитали sum по генераторному выражению, потом max по нему же, и max падает на пустом. Проверить «пуст ли» через if data тоже нельзя: истинность генератора всегда True. Нужен повторный доступ - материализую в список или пересоздаю генератор; данные большие - пересоздание дешевле памяти.

Механизм, классический сценарий бага и обе стратегии лечения с критерием выбора.

На чём валят

  • Второй проход по генератору - пусто и без ошибки; классика «куда делись данные».
  • if data: для генератора не скажет, пуст ли он - генератор всегда truthy.
  • itertools.groupby без сортировки - группы порваны на куски.
  • f.read() ради построчной обработки - файл сам итератор по строкам.
  • Генератор, замыкающий переменную цикла, все инстансы видят последнее значение.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 15, остальные разбираются в тренажёре.

  1. #iterators_generators1 / 5
    Чем yield отличается от return?
    A)Оператор yield немедленно и окончательно завершает функцию точно так же, как и обычный return
    B)return завершает функцию; yield ставит на паузу до следующего next()
    C)Yield можно использовать в функции лишь один раз
    D)Return внутри генератора отдаёт значения по одному в цикле вызова
    показать ответ и разбор
    +B)return завершает функцию; yield ставит на паузу до следующего next()

    // разбор: return завершает функцию и возвращает значение. yield приостанавливает выполнение, отдаёт значение и сохраняет состояние (локальные переменные, точку остановки) до следующего запроса next(). Поэтому функция с yield становится генератором и может продолжить с того места, где остановилась, — хоть в бесконечном цикле.

  2. #iterators_generators2 / 5
    Почему повторный цикл for по одному и тому же генератору не выдаёт ничего?
    A)Один и тот же генератор можно совершенно свободно проходить циклом for сколько угодно раз подряд
    B)Второй проход по генератору выдаёт данные в обратном порядке
    C)Генератор автоматически перезапускается с самого начала при новом цикле
    D)Генератор одноразовый: после исчерпания второй проход по нему пуст
    показать ответ и разбор
    +D)Генератор одноразовый: после исчерпания второй проход по нему пуст

    // разбор: Генератор — одноразовый итератор: пройдя его один раз, вы исчерпали поток, и повторный for не даёт ничего (частый источник багов — «данные вдруг пропали»). Если нужен повторный проход, материализуйте в список (память!), пересоздайте генератор заново или используйте itertools.tee для ограниченного дублирования.

  3. #iterators_generators3 / 5
    Чем генераторное выражение (x for x in ...) отличается от списочного [x for x in ...]?
    A)Генераторное выражение возвращает готовый список, а списочное — кортеж
    B)Оба сразу материализуют все элементы в памяти совершенно одинаково
    C)Списочное строит весь список в памяти; генераторное — лениво по одному
    D)Между ними нет разницы, кроме вида используемых скобок
    показать ответ и разбор
    +C)Списочное строит весь список в памяти; генераторное — лениво по одному

    // разбор: Списочное включение [ ] сразу строит весь список в памяти. Генераторное выражение ( ) возвращает ленивый итератор, отдающий элементы по одному по требованию, — память константна независимо от размера потока. Для больших или бесконечных последовательностей это критично; список берут, когда нужен повторный проход или индексация.

  4. #iterators_generators4 / 5
    Поток из миллиардов чисел, нужны первые 5, удовлетворяющих условию. Почему генератор здесь принципиально лучше списка?
    A)Генератор работает быстрее списка во столько раз, сколько в потоке элементов, за счёт внутреннего кэширования значений
    B)Разницы по памяти между ними нет никакой: генератор всё равно материализует все миллиарды значений в оперативной памяти точно так же, как и список
    C)Генератор автоматически распараллеливает обработку всего потока сразу на все доступные ядра процессора вообще без какой-либо ручной настройки
    D)Генератор ленив: значения считаются по одному по требованию, миллиард в память не грузится, а после 5 находок обход можно прекратить
    показать ответ и разбор
    +D)Генератор ленив: значения считаются по одному по требованию, миллиард в память не грузится, а после 5 находок обход можно прекратить

    // разбор: Генератор вычисляет значения ЛЕНИВО — по одному, только когда их запрашивают, и не хранит всю последовательность в памяти. Для «первых 5 из миллиарда» это критично: список пришлось бы целиком построить в памяти (или это вовсе невозможно — поток бесконечен), а с генератором (+ itertools.islice / break) обрабатывается ровно столько элементов, сколько нужно, с постоянной памятью. Генератор не быстрее «в N раз», не материализует всё и не распараллеливает сам.

  5. #iterators_generators5 / 5
    Что вернёт list(range(2, 10, 3))?
    A)[2, 5, 8]
    B)[2, 5, 8, 10]
    C)[3, 6, 9]
    D)[2, 4, 6, 8]
    показать ответ и разбор
    +A)[2, 5, 8]

    // разбор: range(start, stop, step) начинает со start и идёт с шагом step до stop, не включая его: 2, 5, 8 — следующее 11 уже за границей. Невключённая правая граница — общая конвенция Python (срезы такие же): длина равна stop − start, а соседние диапазоны стыкуются без дыр и перекрытий. Off-by-one на границах — частейший бытовой баг.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.