Генераторы и yield: ленивость и экономия памяти
Один из самых ценных вопросов: что такое генератор, чем yield отличается от return и зачем это нужно.
Генератор — функция с
yield, которая выдаёт значения по одному и «замораживает» своё состояние между выдачами. Это итератор, вычисляемый лениво.
Вопрос: чем yield отличается от return?
Чёткий ответ. return завершает функцию и отдаёт одно значение. yield приостанавливает функцию, отдаёт значение и запоминает, где остановилась; при следующем next() выполнение продолжается с того же места.
def gen():
print("старт")
yield 1
print("середина")
yield 2
print("конец")
g = gen() # код тела ещё НЕ выполнялся
print("генератор создан")
print(next(g)) # дошли до первого yield
print(next(g)) # продолжили со второго
Вывод:
генератор создан старт 1 середина 2
Обратите внимание: «старт» печатается только при первом next(), а не при создании генератора. Между выдачами функция «спит».
Ленивость = экономия памяти
Список хранит все элементы сразу. Генератор хранит только текущее состояние и считает элементы по требованию — поэтому может представлять даже бесконечную последовательность.
def naturals():
n = 1
while True: # бесконечно — но это безопасно
yield n
n += 1
from itertools import islice
first_five = list(islice(naturals(), 5)) # берём только пять
print(first_five)
Вывод:
[1, 2, 3, 4, 5]
Генераторное выражение
Короткий генератор можно записать как выражение в круглых скобках — он не строит список целиком, а выдаёт элементы по одному. Идеально для агрегаций вроде sum.
# круглые скобки — генератор, квадратные — список
gen_expr = (x * x for x in range(5))
print(type(gen_expr).__name__)
print(sum(x * x for x in range(5))) # без промежуточного списка
Вывод:
generator 30
Итог
yieldприостанавливает функцию и запоминает состояние;returnзавершает её.- Генератор ленив: считает по одному элементу, экономит память, может быть бесконечным.
- Генераторное выражение
(... for ...)не строит список целиком — отлично дляsum/any/max.