Генераторы: yield, ленивые вычисления и память

Введение в генераторы: магия yield

Генераторы — это потрясающий инструмент в Python, который позволяет создавать итераторы без лишних затрат памяти. В отличие от списков, генераторы ленивые — они вычисляют значения по требованию!

👉 Простейший пример:

def simple_generator():
    yield 1
    yield 2
    yield 3

gen = simple_generator()
print(next(gen))  # 1
print(next(gen))  # 2

Каждый раз при вызове next() функция продолжает выполнение с места последнего yield. Можно сказать, что генератор "запоминает" своё состояние между вызовами!


Как работают ленивые вычисления 🛋️

Генераторы не хранят все элементы в памяти сразу — они генерируют их на лету. Это особенно полезно при работе с большими наборами данных.

👉 Сравним генератор и список:

# Обычная функция (возвращает список)
def get_squares_list(n):
    squares = []
    for i in range(n):
        squares.append(i ** 2)
    return squares  # Все числа уже в памяти!

# Генератор
def get_squares_gen(n):
    for i in range(n):
        yield i ** 2  # Возвращает ОДНО число за раз

# Использование:
list_squares = get_squares_list(1000000)  # Занимает много памяти!
gen_squares = get_squares_gen(1000000)    # Почти не занимает памяти

📍 Важно! Генератор можно пробежать только один раз — в отличие от списка.


Генераторные выражения: лаконичный синтаксис 📝

Генераторы можно создавать прямо "на лету" с помощью генераторных выражений (аналогично списковым включениям, но с круглыми скобками):

# Списковое включение (создаёт список)
numbers_list = [x * 2 for x in range(10)]

# Генераторное выражение (создаёт генератор)
numbers_gen = (x * 2 for x in range(10))

print(numbers_list)  # [0, 2, 4, 6, ..., 18]
print(numbers_gen)   # <generator object <genexpr> at ...>

👉 Фишка: если нужно просто пробежаться по элементам, генераторное выражение экономит память!


Практическое применение: обработка больших файлов 📂

Генераторы отлично подходят для работы с файлами, которые не помещаются в оперативную память:

def read_large_file(file_path):
    """Лениво читает большой файл построчно."""
    with open(file_path, 'r') as file:
        for line in file:
            yield line.strip()

# Использование:
for line in read_large_file('huge_data.txt'):
    process_line(line)  # Обрабатываем каждую строку отдельно

📌 Важно: файл не загружается полностью в память — обрабатывается строка за строкой.


Бесконечные генераторы и itertools ♾️

Генераторы могут быть бесконечными! Например, генератор бесконечной последовательности:

def infinite_counter(start=0):
    while True:
        yield start
        start += 1

counter = infinite_counter()
print(next(counter))  # 0
print(next(counter))  # 1
# ... и так до бесконечности

Для работы с такими генераторами полезен модуль itertools:

from itertools import islice

# Берём только первые 5 элементов бесконечного генератора
limited = islice(infinite_counter(), 5)
print(list(limited))  # [0, 1, 2, 3, 4]

Когда использовать генераторы? 🤔

Генераторы идеальны, когда:

  1. Работаете с огромными наборами данных (не помещаются в память)
  2. Не нужно хранить все элементы одновременно
  3. Хотите разделить процесс на этапы (конвейерная обработка)

Для более глубокого погружения в тему рекомендую курс Данилы Бежина, где он подробно разбирает генераторы и сопрограммы.


Фишка: цепочки генераторов 🚂

Генераторы можно объединять в конвейеры обработки данных:

def integers():
    """Генератор целых чисел."""
    i = 1
    while True:
        yield i
        i += 1

def squares(seq):
    """Квадраты чисел."""
    for num in seq:
        yield num ** 2

def take(seq, n):
    """Первые n элементов последовательности."""
    for _, num in zip(range(n), seq):
        yield num

# Собираем конвейер:
pipeline = take(squares(integers()), 5)
print(list(pipeline))  # [1, 4, 9, 16, 25]

Каждый генератор в цепочке лениво обрабатывает данные, передавая их следующему звену!


Отладка генераторов 🐞

Для отладки генераторов можно временно преобразовывать их в списки:

gen = (x ** 2 for x in range(5))
print(list(gen))  # [0, 1, 4, 9, 16] — так можно посмотреть все элементы

Но помните: после такого преобразования генератор будет исчерпан!


Под капотом: как работает yield 🔧

Когда интерпретатор встречает yield:

  1. Запоминает текущее состояние функции (все локальные переменные)
  2. Возвращает значение после yield
  3. Приостанавливает выполнение функции
  4. При следующем вызове next() продолжает с того же места

По сути, генераторы — это сопрограммы (coroutines), которые могут приостанавливать своё выполнение.


Главные выводы 🎯

  1. Генераторы — ленивые итераторы, экономящие память
  2. Используйте yield вместо return для создания генераторов
  3. Генераторные выражения (x for x in ...) — краткая форма записи
  4. Идеальны для обработки больших данных и потоковой обработки
  5. Можно создавать конвейеры из генераторов для сложных преобразований

Попробуйте заменить несколько списков генераторами в своих проектах — и сразу увидите разницу в потреблении памяти! 🚀

Скрыть рекламу навсегда

🎥 YouTube: программирование простым языком

Канал, где я спокойно и по шагам объясняю сложные темы — без заумных терминов и лишней теории.

Подходит, если раньше «не заходило», но хочется наконец понять.

▶️ Смотреть курсы на YouTube