Генераторы и итераторы в Python: экономим память и повышаем производительность

В статье рассказываем, как использовать генераторы и итераторы в Python для эффективной работы с большими объёмами данных. Практические примеры обработки файлов, API и конвейеров данных. Научитесь писать код, который масштабируется на любые объёмы информации без переполнения памяти.

РазработкаPython

6 мин

Представьте, что вам нужно обработать файл логов размером в несколько гигабайт. Самое простое решение — загрузить весь файл в память, разбить на строки и начать обработку. Но что, если оперативной памяти не хватит? Или если таких файлов десятки? Именно для таких ситуаций в 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-каналу, где вы найдёте поддержку сообщества, дополнительные материалы и ответы на вопросы от опытных разработчиков.

Учитесь программированию эффективно и с удовольствием!

Комментарии