← Назад к вопросам

За счет чего происходит экономия памяти в генераторе?

1.7 Middle🔥 241 комментариев
#Python Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

За счет чего происходит экономия памяти в генераторах

Генераторы — одна из самых элегантных особенностей Python. Экономия памяти достигается за счёт lazy evaluation (ленивых вычислений) и отсутствия необходимости хранить весь результат в памяти одновременно.

Основное отличие: List vs Generator

# ❌ List: хранит ВСЕ данные в памяти
def get_numbers_list(n):
    result = []  # Список, который будет расти
    for i in range(n):
        result.append(i ** 2)  # Добавляем каждый элемент
    return result  # Возвращаем весь список

# Использование
data = get_numbers_list(1_000_000)
# В памяти ВСЕ 1 миллион элементов одновременно!

# ✅ Generator: вычисляет по одному элементу при запросе
def get_numbers_generator(n):
    for i in range(n):
        yield i ** 2  # Каждый вызов yield возвращает один элемент

# Использование
gen = get_numbers_generator(1_000_000)
# В памяти только текущий элемент (и состояние генератора)

Сравнение памяти: наглядный пример

import sys

# List подход
my_list = [i ** 2 for i in range(1_000_000)]
print(f"List size: {sys.getsizeof(my_list)} bytes")  # ≈ 8 MB (на 64-bit Python)
print(f"Per element: {sys.getsizeof(my_list) / len(my_list):.2f} bytes")  # ≈ 8 bytes

# Generator подход
def my_generator():
    for i in range(1_000_000):
        yield i ** 2

gen = my_generator()
print(f"Generator size: {sys.getsizeof(gen)} bytes")  # ≈ 120 bytes (практически ничего!)

# Сравнение:
# List: 8,000,000 bytes
# Generator: 120 bytes
# Экономия: В 66,666 раз меньше памяти!

Почему генератор экономит память

1. Lazy Evaluation (ленивые вычисления)

def process_data():
    """Генератор не вычисляет значения, пока их не попросят"""
    for i in range(1_000_000):
        print(f"Вычисляю {i}")  # Это выполняется ТОЛЬКО при yield
        yield i ** 2

gen = process_data()
print("Generator создан, но ничего не вычислено!")

# Только когда мы итерируем:
next(gen)  # Вычислит только 0
# Вывод: "Вычисляю 0"

for val in gen:  # Будет вычислять по одному значению за раз
    if val > 100:
        break  # Остановился — остальное не вычислится

# Итог: вычислены только нужные нам значения!

2. Сохранение состояния (State Machine)

Генератор сохраняет своё состояние между вызовами yield'а.

def fibonacci():
    """Генератор Фибоначчи"""
    a, b = 0, 1
    while True:
        yield a  # Возвращаем текущее значение
        # Состояние (a, b) сохраняется между вызовами
        a, b = b, a + b  # Обновляем для следующего вызова

fib = fibonacci()
print(next(fib))  # 0 (a=0, b=1)
print(next(fib))  # 1 (a=1, b=1)
print(next(fib))  # 1 (a=1, b=2)
print(next(fib))  # 2 (a=2, b=3)

# Переменные a, b остаются в памяти, но это ОЧЕНЬ мало
# Вместо списка из 1 миллиона чисел

Как это работает внутри

# Когда ты пишешь функцию с yield
def my_gen():
    print("Step 1")
    yield 1
    print("Step 2")
    yield 2
    print("Step 3")
    yield 3

gen = my_gen()
print("Created generator")

# Первый вызов next()
print("\nCalling next() #1")
val1 = next(gen)  # Выполнит код до первого yield
print(f"Got {val1}")

# Вывод:
# Created generator
# Calling next() #1
# Step 1
# Got 1

# Второй вызов next()
print("\nCalling next() #2")
val2 = next(gen)  # Продолжит ОТ второго yield (не с начала!)
print(f"Got {val2}")

# Вывод:
# Calling next() #2
# Step 2
# Got 2

# Ключевой момент: функция сохраняет свой "instruction pointer"
# и локальные переменные между вызовами next()

Практический пример: обработка больших файлов

# ❌ ПЛОХО: загружаем весь файл в память
def read_large_file_bad(filename):
    with open(filename) as f:
        lines = f.readlines()  # Весь файл в памяти!
    return lines

for line in read_large_file_bad('huge_file.txt'):  # 10 GB файл
    process(line)  # Нужно было 10 GB RAM!

# ✅ ХОРОШО: читаем по одной строке
def read_large_file_good(filename):
    with open(filename) as f:
        for line in f:  # Одна строка в памяти
            yield line.strip()

for line in read_large_file_good('huge_file.txt'):  # 10 GB файл
    process(line)  # Нужно только ~1 KB RAM!

# Реальная экономия: 10,000x меньше памяти!

Цепочка генераторов (Pipeline)

def read_data(filename):
    with open(filename) as f:
        for line in f:
            yield line.strip()

def parse_csv(lines):
    for line in lines:
        parts = line.split(',')
        yield dict(name=parts[0], age=int(parts[1]))

def filter_adults(people):
    for person in people:
        if person['age'] >= 18:
            yield person

def extract_names(people):
    for person in people:
        yield person['name']

# Построим pipeline
names = extract_names(
    filter_adults(
        parse_csv(
            read_data('large_file.csv')
        )
    )
)

# Используем
for name in names:
    print(name)

# Память используется для: одна строка + один dict + одно имя
# Даже если файл 100 GB, каждый шаг обрабатывает одно значение!

Визуализация памяти

# List подход (хранит всё):
Memory: [val0, val1, val2, val3, ..., val999999]
         ^                                     ^
         Всё в памяти одновременно (8 MB+)

# Generator подход (один за раз):
Memory: next() → val1 → next() → val2 → next() → val3
        ^               ^              ^
        Только текущий элемент (несколько байт)

Generator Expression (ещё компактнее)

# List comprehension: создаёт список
list_comp = [i ** 2 for i in range(1_000_000)]  # ≈ 8 MB

# Generator expression: создаёт генератор
gen_expr = (i ** 2 for i in range(1_000_000))  # ≈ 120 bytes

# Синтаксис почти идентичный, но [] vs ()

# Использование
for val in gen_expr:
    print(val)

Когда использовать генераторы

# ✅ Используй генератор, если:
# 1. Большой объём данных (>1000 элементов)
data = (process_row(row) for row in large_dataset)

# 2. Бесконечная последовательность
def infinite_counter():
    i = 0
    while True:
        yield i
        i += 1

# 3. Файлы, потоки, сетевые данные
def read_api_stream():
    for chunk in response.iter_content():
        yield parse(chunk)

# ❌ НЕ используй генератор, если:
# 1. Нужно итерировать несколько раз
data = some_generator()  # Можно итерировать ТОЛЬКо один раз!
for x in data: pass
for x in data: pass  # Не будет элементов (генератор истощен)

# Решение: создай новый генератор или используй list
data = list(some_generator())  # Теперь можно многократно

# 2. Нужно получить длину
len(my_generator())  # TypeError! Генератор не имеет длины
len(list(my_generator()))  # Работает, но требует памяти

Сравнение: память в цифрах

import sys

# 1 миллион целых чисел
my_list = list(range(1_000_000))
my_gen = (x for x in range(1_000_000))

print(f"List: {sys.getsizeof(my_list) / 1_000_000:.2f} MB")
print(f"Generator: {sys.getsizeof(my_gen) / 1_000_000:.2f} MB")

# Результат:
# List: 8.00 MB
# Generator: 0.00 MB (меньше 1 KB)

# В 80,000x меньше памяти!

Ловушка: Однократная итерация

gen = (x for x in range(5))

list(gen)  # [0, 1, 2, 3, 4]
list(gen)  # [] — пусто! Генератор исчерпан

# Почему? Генератор — это **итератор**, а не **итерируемое**
# Он может быть пройден ровно один раз

# Если нужно многократно:
data = list(my_generator())  # Сначала материализуй
for i in range(3):
    for x in data:  # Теперь можно 3 раза
        print(x)

Вывод

Генераторы экономят память за счёт:

  1. Lazy Evaluation — вычисляют только при запросе
  2. Отсутствие хранилища — нет полного списка в памяти
  3. State Machine — сохраняют только текущее состояние
  4. Single-pass — один проход, а не многократное хранение

Результат: 1000x-100000x экономия памяти для больших данных.

Без генераторов невозможно обрабатывать:

  • Гигабайтные файлы
  • Бесконечные потоки
  • Real-time данные
  • Большие датасеты

Генераторы — это суперсила Python для работы с памятью.