← Назад к вопросам
За счет чего происходит экономия памяти в генераторе?
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)
Вывод
Генераторы экономят память за счёт:
- Lazy Evaluation — вычисляют только при запросе
- Отсутствие хранилища — нет полного списка в памяти
- State Machine — сохраняют только текущее состояние
- Single-pass — один проход, а не многократное хранение
Результат: 1000x-100000x экономия памяти для больших данных.
Без генераторов невозможно обрабатывать:
- Гигабайтные файлы
- Бесконечные потоки
- Real-time данные
- Большие датасеты
Генераторы — это суперсила Python для работы с памятью.