Итераторы и генераторы
Протокол итератора, генераторы, yield и ленивые последовательности.
12 вопросов
JuniorТеорияОчень частоЧем отличаются [x for x in y] и (x for x in y)?
Чем отличаются [x for x in y] и (x for x in y)?
[...] — это списковое включение: оно жадно строит в памяти полный list со всеми элементами. (...) — это генераторное выражение: оно возвращает ленивый генератор, выдающий элементы по одному и хранящий лишь текущее состояние.
Типичные ошибки
- ✗Считать, что
(...)строитtuple— оно создаёт генератор, а не tuple - ✗Полагать, что обе формы материализуют полную коллекцию, тогда как генераторное выражение ленивое
- ✗Путать, какая форма ленивая —
[...]жадная, а(...)ленивый генератор
Уточняющие вопросы
- →Как просуммировать миллион квадратов, ни разу не построив список из миллиона элементов?
- →Можно ли индексировать результат
(x for x in y), и почему да или нет?
JuniorТеорияОчень частоЧто такое итерируемый объект (iterable) в Python?
Что такое итерируемый объект (iterable) в Python?
Итерируемый объект выдаёт свои элементы по одному: всё, что определяет __iter__ (или устаревший __getitem__ с индекса 0). iter() превращает его в итератор. list, tuple, str, dict, set и файлы подходят.
Типичные ошибки
- ✗Считать, что каждый iterable — это последовательность с индексацией;
setиdictитерируемы, но неупорядочены/без индексации - ✗Путать iterable с итератором — iterable выдаёт свежий итератор через
iter(), он не сам курсор - ✗Думать, что пользовательские классы не могут быть итерируемыми, хотя
__iter__делает любой класс пригодным дляfor
Уточняющие вопросы
- →Что возвращает
iter()при вызове наlist, и тот же ли это объект каждый раз? - →Как сделать пользовательский класс итерируемым без написания отдельного класса-итератора?
JuniorКодЧастоЧто выведет двойное преобразование генератора в список?
Что выведет двойное преобразование генератора в список?
(1) [0, 1, 2], (2) []. Генераторы ленивы и одноразовы — будучи исчерпанными, они ничего не выдают при втором проходе. Пересоздайте генератор или материализуйте его в список, если нужно пройти более одного раза.
Типичные ошибки
- ✗Ожидать, что генератор перезапустится на втором проходе
- ✗Думать, что исчерпание возбуждает StopIteration для вызывающего list()
- ✗Переиспользовать генератор там, где нужен список
Уточняющие вопросы
- →Почему
list()возвращает[], а не падает на исчерпанном генераторе? - →Когда одноразовый генератор предпочтительнее материализации в список?
JuniorТеорияЧастоЧто такое генераторная функция в Python?
Что такое генераторная функция в Python?
Это функция, в теле которой есть yield. Её вызов не выполняет тело: возвращается генераторный объект (итератор). Тело выполняется лениво, продвигаясь на один шаг при каждом вызове next() на возвращённом генераторе.
Типичные ошибки
- ✗Думать, что вызов генераторной функции выполняет тело — он лишь возвращает генератор, тело идёт по
next() - ✗Считать, что она жадно возвращает
listвсех значений, а не выдаёт их лениво - ✗Считать
returnиyieldвзаимозаменяемыми — толькоyieldделает функцию генератором
Уточняющие вопросы
- →Что происходит с локальными переменными генератора между двумя последовательными вызовами
next()? - →Как запустить выполнение тела функции после того, как генератор создан?
MiddleТеорияЧастоЧто такое итератор, и какой протокол он должен реализовывать?
Что такое итератор, и какой протокол он должен реализовывать?
Итератор реализует __next__ (следующий элемент, бросает StopIteration при исчерпании) и __iter__ (возвращает self), поэтому каждый итератор итерируем. for зовёт iter() раз, затем next() многократно; исчерпавшись, он таким и остаётся.
Типичные ошибки
- ✗Думать, что итератору нужен только
__iter__— он обязан определять__next__и бросатьStopIteration - ✗Полагать, что исчерпанный итератор авто-сбрасывается, тогда как он остаётся исчерпанным навсегда
- ✗Забывать, что
__iter__у итератора возвращаетself, поэтому каждый итератор итерируем
Уточняющие вопросы
- →Почему каждому итератору нужен и метод
__iter__, возвращающийself? - →Что произойдёт при вызове
next()на итераторе, уже бросившемStopIteration?
MiddleТеорияЧастоЧто делает yield внутри генератора?
Что делает yield внутри генератора?
yield замораживает состояние выполнения функции и отдаёт текущее значение вызывающему коду. Следующий next() возобновляет работу сразу после yield, сохраняя все локальные переменные между паузами, так что генератор продолжает с места остановки.
Типичные ошибки
- ✗Воспринимать
yieldкакreturn— он приостанавливает и возобновляет, а не завершает функцию - ✗Думать, что
yieldстроит и возвращает полныйlistвсех значений за один вызов - ✗Полагать, что локальные сбрасываются между паузами, тогда как они сохраняются через каждый
yield
Уточняющие вопросы
- →В какое значение вычисляется само выражение
yieldпри использованииgen.send(x)? - →Как
forузнаёт, что генератор закончился после своего последнегоyield?
JuniorКодИногдаРеализуйте свой reversed генератором и итератором
Реализуйте свой reversed генератором и итератором
Генератором: пройдите индекс от len(seq)-1 до 0 и yield seq[i] — лениво, по одному элементу. Классом-итератором: сохраните последовательность и индекс current в __init__, верните self из __iter__, а в __next__ уменьшайте индекс и возвращайте элемент, бросая StopIteration при достижении 0. Оба эквивалентны; генератор просто короче.
Типичные ошибки
- ✗Опускать
__iter__, возвращающийself, из-за чего объект не работает вfor - ✗Забывать бросать
StopIteration, когда индекс исчерпан - ✗Материализовать развёрнутую копию вместо ленивой выдачи по индексу
Уточняющие вопросы
- →Почему
__iter__должен возвращатьself, чтобы итератор работал вfor? - →Что сигнализирует
StopIterationи кто его ловит?
MiddleКодИногдаРазверните произвольно вложенный список
Разверните произвольно вложенный список
Рекурсия через генератор: для каждого элемента, если это list, yield from flatten(item); иначе yield item. yield from делегирует под-генератору, лениво разворачивая любую глубину. Материализуйте результат через list(flatten(...)), когда нужен конкретный список.
Типичные ошибки
- ✗Использовать плоское включение, разворачивающее лишь один уровень
- ✗Считать, что
sum(nested, [])рекурсирует в более глубокую вложенность - ✗Полагать, что у списков есть встроенный метод
flatten
Уточняющие вопросы
- →Что делает
yield from, чего достиг бы и обычный циклfor ... yield? - →Как расширить это, чтобы разворачивать кортежи и другие итерируемые, а не только списки?
MiddleТеорияИногдаЧто делают методы генератора send, throw и close?
Что делают методы генератора send, throw и close?
send(x) возобновляет генератор и делает так, что приостановленный yield вычисляется в x, поэтому next(g) равен send(None). throw(exc) бросает exc в точке yield, чтобы генератор поймал её или прибрался. close() бросает там GeneratorExit, завершая его.
Типичные ошибки
- ✗Звать
send(x)до того, как генератор «прогрет» черезnext()илиsend(None) - ✗Ждать, что
throwбросит после завершения генератора, а не в текущемyield - ✗Считать, что обычные итераторы вроде
listилиrangeподдерживаютsend/throw/close
Уточняющие вопросы
- →Почему генератор нужно «прогреть» через
next()перед первымsend(x)? - →Как генератору обработать
GeneratorExitотclose()в блокеfinally?
MiddleТеорияИногдаПочему генератор экономнее по памяти, чем списковое включение?
Почему генератор экономнее по памяти, чем списковое включение?
Списковое включение выделяет и держит все n элементов сразу (память O(n)), тогда как генератор держит лишь состояние текущего кадра и выдаёт каждый элемент по требованию (память O(1)). Поэтому генератор может обрабатывать потоки больше RAM или даже бесконечные последовательности, где список исчерпал бы память.
Типичные ошибки
- ✗Думать, что пик памяти всё равно O(n), ведь Python якобы материализует результат до выдачи
- ✗Считать, что расход генератора не опустится ниже O(n), раз источник уже лежит в RAM
- ✗Называть память генератора O(log n) или O(√n) вместо настоящего O(1) состояния кадра
Уточняющие вопросы
- →Как генератор может обработать лог-файл на 50 ГБ на машине с 8 ГБ RAM?
- →Когда списковое включение всё же окажется лучшим выбором, несмотря на расход памяти?
SeniorТеорияИногдаЧто делает yield from с подгенератором?
Что делает yield from с подгенератором?
yield from subgen делегирует ему: выдаёт каждое значение подгенератора, прозрачно пробрасывает send и throw, а его результатом становится return-значение подгенератора (через StopIteration). Он уплощает вложенные генераторы.
Типичные ошибки
- ✗Думать, что
yield fromвыдаёт сам объект подгенератора, а не его отдельные значения - ✗Считать, что это просто
for x in sub: yield x— он ещё пробрасываетsend/throwиreturn-значение - ✗Забывать, что
return-значение подгенератора становится результатом выраженияyield from
Уточняющие вопросы
- →Где всплывает
return-значение подгенератора и как его захватить? - →Как
yield fromупрощает написание рекурсивного генератора для уплощения дерева?
SeniorТеорияРедкоКак iter() откатывается к __getitem__, когда __iter__ отсутствует?
Как iter() откатывается к __getitem__, когда __iter__ отсутствует?
iter(obj) сначала ищет __iter__. Если его нет, но есть __getitem__, строится итератор, зовущий obj[0], obj[1], … до IndexError (устаревший протокол последовательности). Если нет ни одного метода, бросается TypeError.
Типичные ошибки
- ✗Думать, что отсутствие
__iter__всегда бросает сразу, игнорируя откат к__getitem__ - ✗Считать, что откат использует
__len__или заглушкуNone, а не остановку поIndexError - ✗Забывать, что без
__iter__и__getitem__методiter()бросаетTypeError
Уточняющие вопросы
- →Почему откат через
__getitem__обязан начинаться именно с индекса 0? - →Как итератор отката понимает, что пора остановиться, и какое исключение сигналит о конце?