Генераторы: 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]
Когда использовать генераторы? 🤔
Генераторы идеальны, когда:
- Работаете с огромными наборами данных (не помещаются в память)
- Не нужно хранить все элементы одновременно
- Хотите разделить процесс на этапы (конвейерная обработка)
Для более глубокого погружения в тему рекомендую курс Данилы Бежина, где он подробно разбирает генераторы и сопрограммы.
Фишка: цепочки генераторов 🚂
Генераторы можно объединять в конвейеры обработки данных:
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:
- Запоминает текущее состояние функции (все локальные переменные)
- Возвращает значение после
yield - Приостанавливает выполнение функции
- При следующем вызове
next()продолжает с того же места
По сути, генераторы — это сопрограммы (coroutines), которые могут приостанавливать своё выполнение.
Главные выводы 🎯
- Генераторы — ленивые итераторы, экономящие память
- Используйте
yieldвместоreturnдля создания генераторов - Генераторные выражения
(x for x in ...)— краткая форма записи - Идеальны для обработки больших данных и потоковой обработки
- Можно создавать конвейеры из генераторов для сложных преобразований
Попробуйте заменить несколько списков генераторами в своих проектах — и сразу увидите разницу в потреблении памяти! 🚀