Вы когда-нибудь пробовали создать список из миллиона чисел? Память начинает трещать по швам. А если нужно прочитать огромный лог-файл — загружать его целиком? Нет, есть способ лучше. Знакомьтесь: итераторы и генераторы. Они позволяют обрабатывать данные по одному элементу, не храня всё сразу. Это экономит память, ускоряет запуск и делает код чище.
1. Что такое итератор?
Итератор — это объект, который умеет выдавать элементы по одному. У него есть метод __next__(): при каждом вызове возвращается следующий элемент, а когда элементы кончаются — бросается исключение StopIteration. Также итератор должен иметь метод __iter__(), возвращающий сам себя.
Самый простой пример — список, но сам список не итератор, а итерируемый объект. Итератор получается через функцию iter():
numbers = [1, 2, 3]
it = iter(numbers) # получаем итератор
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
print(next(it)) # StopIteration
Цикл for x in numbers внутри делает то же самое: вызывает iter(), затем next() до StopIteration.
2. Создаём свой итератор
Класс становится итератором, если реализует методы __iter__ и __next__. Например, итератор, который выдает числа от 0 до N:
class CountDown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current < 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for i in CountDown(5):
print(i) # 5 4 3 2 1 0
Такой итератор не хранит все числа в памяти — только текущее значение.
3. Генераторы — итераторы одной строкой
Писать класс с __next__ для простых задач — излишне. Генератор создаётся с помощью функции, в которой есть ключевое слово yield. Вместо return (который завершает функцию) yield приостанавливает функцию, запоминает её состояние и возвращает значение. При следующем вызове функция продолжается с того же места.
def count_down(start):
while start >= 0:
yield start
start -= 1
for i in count_down(5):
print(i) # 5 4 3 2 1 0
Магия: в памяти не хранится список, числа генерируются по требованию.
4. Бесконечные генераторы
Генератор может работать бесконечно — например, выдавать все натуральные числа или числа Фибоначчи. Главное — правильно выйти из цикла (или использовать break на стороне вызова).
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
for _ in range(10):
print(next(fib)) # 0 1 1 2 3 5 8 13 21 34
Это идеальный способ работать с последовательностями, которые теоретически бесконечны.
5. Генераторные выражения
Как списковые включения, но не в квадратных, а в круглых скобках. Результат — генератор, а не список.
squares = (x*x for x in range(1000000)) # не создаёт миллион элементов
for s in squares:
if s > 100:
break
print(s)
Генераторное выражение занимает минимум памяти и начинает выдавать элементы сразу, без задержки на построение всего списка.
6. Где это реально нужно?
- Чтение больших файлов — построчно, не загружая весь файл в память.
- Обработка потоков данных (данные с датчиков, логи сервера).
- Генерация бесконечных последовательностей (ID, случайные числа).
- Пагинация в API (подгружать следующую страницу по требованию).
Пример: чтение большого файла построчно с фильтрацией.
def read_log_errors(filename):
with open(filename, "r", encoding="utf-8") as f:
for line in f:
if "ERROR" in line:
yield line.strip()
for error_line in read_log_errors("server.log"):
print(error_line) # обрабатываем ошибки по одной
Файл может быть 10 ГБ — программа не упадёт.
7. Генераторы как ленивые списки
Генераторы вычисляют значение только в момент запроса. Это называется ленивыми вычислениями. Преимущество: можно работать с бесконечными данными, а также экономить ресурсы, если нужна только часть последовательности.
def first_n(generator, n):
result = []
for i, value in enumerate(generator):
if i >= n:
break
result.append(value)
return result
# Возьмём первые 10 чисел Фибоначчи
print(first_n(fibonacci(), 10))
8. Типичные ошибки новичков
❌ Одноразовость генератора
Генератор можно обойти только один раз. Второй цикл for не выдаст элементов.
gen = (x for x in range(5))
print(list(gen)) # [0,1,2,3,4]
print(list(gen)) # []
Если нужно использовать несколько раз — превратите в список (но тогда теряется смысл).
❌ Забыли yield и написали return
return в генераторе завершает генерацию и бросает StopIteration. Если нужно вернуть значение при завершении, можно использовать return value в Python 3.3+, но это редкий случай.
❌ Бесконечный цикл без выхода
Если в генераторе бесконечный while True и нет break или условия, вызывающий код должен сам знать, когда остановиться. Иначе получите бесконечный цикл.
❌ Смешивание yield и return в одной функции
Технически можно, но новичков это сбивает с толку. Лучше держать функции либо генераторами (с yield), либо обычными (с return).
9. Живой пример: ленивая обработка данных о продажах
Представьте, что у вас есть файл sales.txt с миллионом строк вида "товар,цена,количество". Нужно посчитать общую выручку, не загружая всё в память.
def read_sales(filename):
with open(filename, "r", encoding="utf-8") as f:
for line in f:
try:
product, price, qty = line.strip().split(",")
yield product, float(price), int(qty)
except ValueError:
continue # пропускаем битые строки
def total_revenue(sales_gen):
total = 0.0
for _, price, qty in sales_gen:
total += price * qty
return total
revenue = total_revenue(read_sales("sales.txt"))
print(f"Общая выручка: {revenue:.2f}")
Генератор read_sales читает файл построчно и отдаёт распарсенные данные. Никакого гигантского списка в памяти. Программа обработает файл любого размера.
Заключение
Итераторы и генераторы — это мощный инструмент для написания эффективного кода. Вы научились:
- создавать собственные итераторы через классы,
- использовать генераторы с yield для ленивых вычислений,
- применять генераторные выражения для краткости,
- читать большие файлы и обрабатывать бесконечные последовательности.
Следующая статья будет посвящена декораторам — способу обогащать функции без изменения их кода. А пока — попробуйте переписать любой свой скрипт, который работал с большим списком, на генератор. Замерьте память — и вы станете фанатом лени.
Делитесь в комментариях, где вы применили генераторы и насколько это улучшило производительность.
Статья подготовлена для канала «Код как искусство». Подписывайтесь, чтобы писать код, который не жрёт память.