Функция, которая делает паузы
Генератор выглядит как обычная функция, но вместо того чтобы вычислить результат целиком и вернуть его, он выдаёт одно значение за раз, замирая между выдачами до тех пор, пока кто-то не попросит следующее.
Самый простой:
Обрати внимание на yield вместо return. Когда for впервые просит значение, Python выполняет тело до yield 1. Функция замирает ровно там, отдаёт 1 циклу и помнит, где остановилась, — переменные и всё. Следующая итерация подхватывает с того же места: current += 1, обратно к while, yield 2. И так до тех пор, пока условие не станет ложным, — тогда генератор просто останавливается.
Этот «пауза и продолжение» — и есть весь трюк.
Почему не просто построить список?
Потому что версия со списком выделяет все значения заранее:
Для пяти элементов нормально. Теперь представь, что нужно 50 миллионов целых, а интересен только первый, подходящий под условие. Версия со списком выделит 50 миллионов int-ов, и большую часть ты выбросишь. Версия с генератором создаёт ровно столько, сколько потребляет вызывающий. Когда for находит нужное и делает break, генератор просто останавливается.
Паттерн стоит усвоить: генераторы позволяют писать код итерации, не решая заранее, сколько результата тебе понадобится.
Generator-выражения
Если ты писал списковое включение, синтаксис ты уже знаешь — замени квадратные скобки на круглые:
squares_gen пока ничего не вычислил. Это просто рецепт. Итерация запускает рецепт пошагово.
Generator-выражения идеальны как аргументы функций, потребляющих итерируемое:
Никаких промежуточных списков. sum, max и any читают значения по одному — именно то, что им и надо.
Чтение большого файла построчно
Канонический реальный кейс для генераторов — обработать файл, который не помещается в память:
def parse_log_lines(path):
with open(path) as f:
for line in f:
if line.startswith("ERROR"):
yield line.rstrip()
for error in parse_log_lines("app.log"):
print(error)
Файл читается лениво. Каждый вызов к генератору вытаскивает одну строку с диска, фильтрует и выдаёт. Расход памяти остаётся плоским независимо от размера файла.
Один раз и всё
У генератора один проход. После того как проитерировался до конца, он исчерпан:
Второй цикл не распечатает ничего. Генератору нечего отдать.
Если нужна итерация больше одного раза — либо вызови функцию-генератор заново, чтобы получить свежий, либо материализуй последовательность через list(...) и итерируй список повторно. Выбирай по цене: пересоздать дёшево, если работа дешёвая; список нормален, если последовательность маленькая.
next() и ручная итерация
for не обязателен. next() вытаскивает одно значение за раз:
StopIteration — это как генератор сигнализирует «я закончил». Циклы for его тихо ловят. В ручном коде можно передать значение по умолчанию — next(gen, default), — чтобы избежать исключения.
Бесконечные генераторы
Поскольку значения производятся по требованию, генератор может представлять последовательность без конца — пока потребитель не перестанет просить:
while True с yield внутри не подвешивает программу — это означает «если кто-то продолжает просить, продолжай выдавать». Потребитель решает, когда останавливаться.
Такой паттерн всплывает в стриминге данных, event-loop-ах и везде, где значения тянут из источника без заранее известной длины.
yield from: делегирование другой итерируемой
Если генератор хочет выдавать каждое значение из другой итерируемой, yield from делает это в одну строку:
Без yield from пришлось бы писать вложенный for с yield x внутри. Он также правильно пробрасывает вызовы send() и throw(), если их используешь, — но для повседневного кода считай это «выдай каждое значение из этой штуки».
Когда тянуться к генератору
Три сигнала того, что генератор — правильный инструмент:
- Последовательность большая, возможно бесконечная или дорогая для производства целиком.
- Потребитель может остановиться до конца (например,
breakпо первому совпадению). - Ты хочешь цепочку преобразований — фильтр, map, take — без построения промежуточных списков.
А когда не надо:
- Нужен случайный доступ (
seq[42]). Генераторы идут только вперёд. - Нужно пройти ту же последовательность несколько раз. Используй список.
- Последовательность маленькая и уже у тебя. Списковое включение проще.
Генераторы, списковые включения и обычные списки — каждый правильный ответ для разных задач. Навык — выбирать, не сильно задумываясь. Самый быстрый способ развить интуицию — замечать для каждой итерации, которую пишешь, что лучше: «сначала произвести всё» или «производить по одному».
Дальше: контекстные менеджеры подробнее
Ты уже видел большинство идиом итерации Python. Контекстные менеджеры — оператор with — следующие, и они хорошо сочетаются с генераторами для стриминга данных из файлов и сетевых соединений.
Часто задаваемые вопросы
Что такое генератор в Python?
Генератор — это функция, производящая значения по одному и делающая паузы между ними. Пишется def, как обычная функция, но вместо return — yield. Вызов возвращает объект-генератор; каждая итерация for или каждый next() запускает тело до следующего yield.
В чём разница между списком и генератором?
Список держит все элементы в памяти сразу. Генератор вычисляет элементы по требованию и забывает их после использования. Для больших или бесконечных последовательностей генераторы занимают небольшую фиксированную память; для маленьких результатов, нужных многократно, лучше список.
Можно ли итерироваться по генератору дважды?
Нет. После одного полного прохода генератор исчерпан — второй for по нему не даст ничего. Нужно итерироваться больше раза — либо вызывай функцию-генератор заново и получай свежий, либо материализуй результаты в список.