Добавить в корзинуПозвонить
Найти в Дзене
Код как искусство

Итераторы и генераторы: как не взорвать память и писать элегантный код

Вы когда-нибудь пробовали создать список из миллиона чисел? Память начинает трещать по швам. А если нужно прочитать огромный лог-файл — загружать его целиком? Нет, есть способ лучше. Знакомьтесь: итераторы и генераторы. Они позволяют обрабатывать данные по одному элементу, не храня всё сразу. Это экономит память, ускоряет запуск и делает код чище. Итератор — это объект, который умеет выдавать элементы по одному. У него есть метод __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 внутри делает то же самое: вызывает it
Оглавление

Вы когда-нибудь пробовали создать список из миллиона чисел? Память начинает трещать по швам. А если нужно прочитать огромный лог-файл — загружать его целиком? Нет, есть способ лучше. Знакомьтесь: итераторы и генераторы. Они позволяют обрабатывать данные по одному элементу, не храня всё сразу. Это экономит память, ускоряет запуск и делает код чище.

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 для ленивых вычислений,
  • применять генераторные выражения для краткости,
  • читать большие файлы и обрабатывать бесконечные последовательности.

Следующая статья будет посвящена декораторам — способу обогащать функции без изменения их кода. А пока — попробуйте переписать любой свой скрипт, который работал с большим списком, на генератор. Замерьте память — и вы станете фанатом лени.

Делитесь в комментариях, где вы применили генераторы и насколько это улучшило производительность.

Статья подготовлена для канала «Код как искусство». Подписывайтесь, чтобы писать код, который не жрёт память.