Генераторы и итераторы в Python: экономим память и повышаем производительность
В статье рассказываем, как использовать генераторы и итераторы в Python для эффективной работы с большими объёмами данных. Практические примеры обработки файлов, API и конвейеров данных. Научитесь писать код, который масштабируется на любые объёмы информации без переполнения памяти.
Представьте, что вам нужно обработать файл логов размером в несколько гигабайт. Самое простое решение — загрузить весь файл в память, разбить на строки и начать обработку. Но что, если оперативной памяти не хватит? Или если таких файлов десятки? Именно для таких ситуаций в Python существуют генераторы и итераторы — мощные инструменты, которые позволяют работать с большими объёмами данных, не загружая их полностью в память.
Что такое итераторы?
Итератор — это объект, который позволяет перебирать элементы коллекции по одному, не загружая всю коллекцию в память сразу. В Python любой объект, который реализует метод __iter__() и __next__(), является итератором.
Когда вы пишете for item in collection, Python неявно вызывает iter(collection) для получения итератора, а затем многократно вызывает next() для получения следующего элемента, пока не будет выброшено исключение StopIteration.
# Пример простого итератора
class CountDown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
self.current -= 1
return self.current + 1
# Использование
counter = CountDown(5)
for num in counter:
print(num) # Выведет: 5, 4, 3, 2, 1
Генераторы: итераторы на стероидах
Генераторы — это упрощённый способ создания итераторов. Вместо написания класса с методами __iter__() и __next__(), вы просто создаёте функцию с ключевым словом yield. Генератор автоматически сохраняет своё состояние между вызовами и возобновляет выполнение с того места, где остановился.
def count_down(start):
while start > 0:
yield start
start -= 1
# Использование
for num in count_down(5):
print(num) # Выведет: 5, 4, 3, 2, 1Преимущество очевидно: код стал компактнее и понятнее, а функциональность осталась той же.
Почему это экономит память?
Главное преимущество генераторов — ленивые вычисления. Элементы создаются только тогда, когда они нужны, и сразу же могут быть обработаны и забыты. Давайте посмотрим на конкретный пример:
# Плохо: загружаем всё в память
def read_large_file_bad(file_path):
with open(file_path, 'r') as file:
lines = file.readlines() # Весь файл в памяти!
return [line.strip().upper() for line in lines]
# Хорошо: обрабатываем построчно
def read_large_file_good(file_path):
with open(file_path, 'r') as file:
for line in file: # file уже итератор!
yield line.strip().upper()
# Использование
for processed_line in read_large_file_good('huge_log.txt'):
# Обрабатываем по одной строке
process(processed_line)В первом случае, если файл весит 2 ГБ, ваша программа займёт как минимум 2 ГБ оперативной памяти. Во втором случае — всего несколько килобайт, независимо от размера файла.

Практические примеры использования.
Обработка данных из API
Часто API возвращают данные постранично. Генератор может скрыть эту сложность:
def fetch_all_users(api_client, page_size=100):
page = 1
while True:
users = api_client.get_users(page=page, size=page_size)
if not users:
break
for user in users:
yield user
page += 1
# Использование
for user in fetch_all_users(api):
print(user['name']) # Обрабатываем пользователей по одномуБесконечные последовательности
Генераторы позволяют создавать бесконечные последовательности без риска переполнения памяти:
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# Можно брать сколько нужно элементов
from itertools import islice
first_ten = list(islice(fibonacci(), 10))
print(first_ten) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]Конвейеры обработки данных
Генераторы отлично подходят для создания цепочек обработки:
def read_csv(file_path):
with open(file_path) as f:
for line in f:
yield line.strip().split(',')
def filter_by_status(rows, status):
for row in rows:
if row[2] == status: # статус в третьей колонке
yield row
def extract_emails(rows):
for row in rows:
yield row[1] # email во второй колонке
# Собираем конвейер
rows = read_csv('users.csv')
active_users = filter_by_status(rows, 'active')
emails = extract_emails(active_users)
# Только здесь начинается реальная обработка
for email in emails:
send_notification(email)Красота этого подхода в том, что каждый этап конвейера независим и тестируем, а память используется только для одного элемента.
Выражения-генераторы
Помимо функций-генераторов, Python поддерживает выражения-генераторы — компактный синтаксис для создания генераторов:
# List comprehension — создаёт список в памяти
squares_list = [x**2 for x in range(1000000)] # Занимает много памяти
# Generator expression — создаёт генератор
squares_gen = (x**2 for x in range(1000000)) # Почти не занимает памяти
# Можно использовать в функциях
total = sum(x**2 for x in range(1000000)) # Эффективно!Обратите внимание: вместо квадратных скобок используются круглые. Это одно из самых простых и эффективных изменений, которые вы можете внести в свой код для экономии памяти.
Когда НЕ стоит использовать генераторы?
Несмотря на все преимущества, генераторы подходят не всегда:
Когда нужен многократный доступ к данным: генератор можно пройти только один раз. Если вам нужно обращаться к данным несколько раз, лучше использовать список.
Когда важна скорость доступа: получение элемента по индексу из списка происходит мгновенно, а генератор придётся перебирать с начала.
Когда данные помещаются в память: если у вас небольшой массив данных, накладные расходы на создание генератора могут быть неоправданными.
Когда нужно знать длину: len() не работает с генераторами, и вычисление длины требует полного перебора.
Продвинутые техники.
Делегирование генераторов
С помощью yield from можно делегировать выполнение другому генератору:
def read_multiple_files(*file_paths):
for path in file_paths:
with open(path) as f:
yield from f # Делегируем построчное чтение
# Читаем несколько файлов как один поток
for line in read_multiple_files('log1.txt', 'log2.txt', 'log3.txt'):
process(line)Двусторонняя связь с генераторами
Генераторы могут не только отдавать значения через yield, но и принимать их через метод send():
def running_average():
total = 0
count = 0
average = None
while True:
value = yield average
total += value
count += 1
average = total / count
# Использование
avg = running_average()
next(avg) # Запускаем генератор
print(avg.send(10)) # 10.0
print(avg.send(20)) # 15.0
print(avg.send(30)) # 20.0Измеряем эффективность
Давайте посмотрим на реальную разницу в использовании памяти:
import sys
# Список
numbers_list = [x for x in range(1000000)]
print(f"Список: {sys.getsizeof(numbers_list) / 1024 / 1024:.2f} МБ")
# Генератор
numbers_gen = (x for x in range(1000000))
print(f"Генератор: {sys.getsizeof(numbers_gen) / 1024:.2f} КБ")На моей машине результат показывает разницу в тысячи раз: список занимает около 8 МБ, а генератор — менее 0.1 КБ.
Заключение
Генераторы и итераторы — это не просто синтаксический сахар, а фундаментальный инструмент для работы с большими объёмами данных. Они позволяют писать более эффективный код, который масштабируется на данные любого размера. Понимание того, когда и как использовать генераторы, отличает опытного разработчика от новичка.
Начните с простого: замените list comprehensions на generator expressions там, где вам не нужен многократный доступ к данным. Затем попробуйте написать функцию-генератор для обработки файлов. И постепенно вы обнаружите, что думаете о потоках данных, а не о коллекциях в памяти.
Приложение Кодик предлагает структурированные курсы для начинающих разработчиков, охватывающие Python, JavaScript, HTML, CSS и многие другие технологии.
Присоединяйтесь к нашему Telegram-каналу, где вы найдёте поддержку сообщества, дополнительные материалы и ответы на вопросы от опытных разработчиков.
Учитесь программированию эффективно и с удовольствием!