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

Python: генераторы

Генератор выдаёт значения по одному и только тогда, когда их просят. Он не хранит всю последовательность в памяти, поэтому подходит для больших файлов и бесконечных потоков. Плата за это — генератор одноразовый: пройти по нему дважды не получится.

Обновлено
В этой статье

Функция, которая ставит себя на паузу#

Обычная функция выполняется до return и забывает своё состояние. Функция с
yield ведёт себя иначе: она отдаёт значение и замирает, сохранив всё — место
остановки, локальные переменные, — до следующего запроса.

def countdown(n):
    while n > 0:
        yield n
        n -= 1
    print("готово")

gen = countdown(3)
print(gen)          # <generator object countdown at 0x...>
print(next(gen))    # 3
print(next(gen))    # 2
print(next(gen))    # 1
next(gen)           # печатает «готово», затем StopIteration

Вызов countdown(3) не выполняет ни одной строки тела — он только создаёт
объект-генератор. Первая строка выполнится при первом next. Когда тело
дошло до конца, генератор бросает StopIteration; цикл for ловит это
исключение сам, поэтому в обычном коде его не видно:

for x in countdown(3):
    print(x, end=" ")   # 3 2 1 готово

Генераторные выражения#

Если логика умещается в одно выражение, отдельная функция не нужна. Синтаксис
тот же, что у списочного выражения, но в круглых скобках:

squares_list = [x * x for x in range(5)]      # список, считается целиком
squares_gen = (x * x for x in range(5))       # генератор, ничего не посчитано
print(squares_list)                            # [0, 1, 4, 9, 16]
print(squares_gen)                             # <generator object ...>
print(sum(x * x for x in range(5)))            # 30

Когда генераторное выражение — единственный аргумент функции, лишние скобки
можно не писать: sum(x * x for x in range(5)). Это самый частый способ
применения: посчитать сумму, максимум или any/all, не строя список.

lines = ["1", "", "3"]
print(any(line == "" for line in lines))    # True
print(max(len(line) for line in lines))     # 1

Зачем это нужно#

Память. Списочное выражение по миллиону элементов создаёт миллион объектов
сразу; генератор держит один. Разницу видно инструментом sys.getsizeof: у
списка размер растёт с длиной, у генератора остаётся константным.

Ранний выход. Если ответ нашёлся на третьем элементе, остальные не будут
вычислены вовсе:

def logged(items):
    for item in items:
        print("считаю", item)
        yield item * 2

for value in logged([1, 2, 3]):
    if value >= 4:
        break
print("вышли")
  # считаю 1 / считаю 2 / вышли

Бесконечность. Генератор может не заканчиваться никогда — это допустимо,
пока его кто-то ограничивает:

def naturals():
    n = 1
    while True:
        yield n
        n += 1

from itertools import islice
print(list(islice(naturals(), 5)))   # [1, 2, 3, 4, 5]

Конструкция yield from передаёт наружу все значения другого генератора или
последовательности целиком, без явного цикла: yield from range(3) выдаст
0, 1, 2.

Подводные камни#

Генератор одноразовый. После того как его прошли, он пуст:

gen = (x for x in [1, 2, 3])
print(list(gen))   # [1, 2, 3]
print(list(gen))   # []

Это бьёт в коде, где результат сначала печатают, а потом хотят посчитать. Если
нужна повторная обработка — сохрани в список или собери генератор заново.

len не работает. У генератора нет длины: len(gen) даёт
TypeError: object of type 'generator' has no len(). Считать элементы можно
только пройдя по ним: sum(1 for _ in gen) — и после этого генератор пуст.

Индексов тоже нет. gen[0] невозможен; берут next(gen) или
itertools.islice.

Позднее связывание. Генераторное выражение берёт значения из внешних
переменных в момент перебора, а не в момент создания. Если переменная между
этими моментами изменилась, результат изменится тоже.

Маленькая практика: напиши генератор read_numbers(path), который выдаёт по
одному числу из файла, и посчитай их сумму через sum(...), не собирая
список. Чтение построчно разобрано в статье про
чтение файла, а устройство самой функции с
параметрами — в статье про функции.

План по этапам

  1. Первый yieldНаписать генератор из трёх значений и вызвать next четыре раза, дождавшись StopIteration.
  2. Сравнить со спискомСобрать одни и те же данные списочным и генераторным выражением, напечатать оба объекта.
  3. Посчитать без спискаНайти сумму квадратов первой тысячи чисел через sum с генераторным выражением.
  4. Увидеть ленивостьДобавить print внутрь генератора и прервать цикл через break — убедиться, что лишнее не считалось.
  5. Наступить на одноразовостьПройти по генератору дважды подряд и объяснить пустой результат второго прохода.

Начать изучать эту тему у себя

План ляжет в твой репозиторий: отмечай этапы, веди конспект — история изменений покажет, как ты продвинулся.

Начать план

Проверь себя

1.Что выведет: g = (x for x in [1, 2, 3]); list(g); print(len(list(g)))?

2.Что вернёт sum(x * x for x in range(5))?

3.Что напечатает print(type((x for x in range(3))).__name__)?

4.Сколько значений выдаст генератор до StopIteration: def g(): yield from range(4)?

Источники

Было полезно?

Ещё темы

Программирование и IT Python: регулярные выражения Регулярное выражение — шаблон, описывающий множество строк. В Python с ними работает модуль `re` из стандартной библиотеки. Шаблон пишут в строке с префиксом `r`, ищут через `re.search`, а результат достают из объекта совпадения. Всё остальное — детали языка шаблонов. Программирование и IT Как изучить Python с нуля Python хорош для первого знакомства с программированием: код читается почти как текст, а стандартная библиотека закрывает большинство бытовых задач. Этот план ведёт от установки интерпретатора до собственных скриптов, покрытых тестами, — примерно за четыре месяца при занятиях около часа в день. Программирование и IT Регулярные выражения Регулярное выражение — это компактное описание множества строк. Один и тот же синтаксис работает в grep, в редакторе кода, в JavaScript, PHP, Java и Python, поэтому выучить его достаточно один раз. Здесь разобран сам язык шаблонов, а не библиотека конкретного языка. Программирование и IT Python: задачи с решениями Подборка задач по возрастанию сложности — от разворота строки до генератора и декоратора-счётчика. Сначала решай сам и только потом открывай разбор: сравнение своего кода с чужим даёт больше, чем чтение готового. Все решения проверены на Python 3. Программирование и IT Python: срезы Срез вырезает из последовательности кусок по правилу «с какого индекса, до какого, с каким шагом». Запись компактная, но у неё два места, где почти все ошибаются: правая граница не включается, а при отрицательном шаге границы меняются местами. Разберём оба. Программирование и IT Python: списки Список — изменяемый упорядоченный набор значений произвольных типов. Он же главный источник неожиданностей у начинающих: присваивание не копирует список, sort ничего не возвращает, а удаление элементов прямо в цикле пропускает половину. Разберём по порядку.

Ещё сценарии