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

Какие преимущества у генераторов перед списками?

1.0 Junior🔥 131 комментариев
#Python

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

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

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

Преимущества генераторов перед списками (Generators vs Lists)

1. Память (Memory Efficiency)

Список хранит ВСЕ элементы в памяти одновременно.

# Список: O(n) память
my_list = [i for i in range(1000000)]  # 1 млн элементов
print(sys.getsizeof(my_list))  # ≈ 8 мегабайт

# Итератор через список: выделена вся память сразу
for item in my_list:
    print(item)

Генератор создаёт элементы по требованию (lazy evaluation).

# Генератор: O(1) память
def my_generator():
    for i in range(1000000):
        yield i  # Создаёт элемент ПО ТРЕБОВАНИЮ

# Память практически не растёт
gen = my_generator()
for item in gen:
    print(item)

print(sys.getsizeof(gen))  # ≈ 100 байт (только объект генератора)

Сравнение:

import sys

# Список: 8 MB
my_list = list(range(1000000))
print(f"List size: {sys.getsizeof(my_list) / 1024 / 1024:.2f} MB")

# Генератор: 0.0001 MB
def my_gen():
    for i in range(1000000):
        yield i

gen = my_gen()
print(f"Generator size: {sys.getsizeof(gen) / 1024 / 1024:.4f} MB")

# ✓ Генератор экономит память в 80,000x раз!

2. Скорость (Performance)

Список: нужно время на создание

import time

# Список: 0.5 сек на создание
start = time.time()
my_list = [i for i in range(10000000)]
list_time = time.time() - start
print(f"List creation: {list_time:.3f}s")  # ~0.5s

# Генератор: мгновенно
start = time.time()
def my_gen():
    for i in range(10000000):
        yield i

gen = my_gen()
gen_time = time.time() - start
print(f"Generator creation: {gen_time:.6f}s")  # ~0.0001s

3. Ленивое вычисление (Lazy Evaluation)

Генератор вычисляет только то, что нужно ПРЯМО СЕЙЧАС.

# Список: вычислит ВСЕ, хотя может понадобиться только первые
my_list = [complex_calculation(i) for i in range(1000000)]
first_ten = my_list[:10]  # Но мы используем только первые 10!
# Выбросили 9999990 вычислений в мусор

# Генератор: вычислит только первые 10
def my_gen():
    for i in range(1000000):
        yield complex_calculation(i)

gen = my_gen()
first_ten = list(itertools.islice(gen, 10))  # Вычислены только 10!

Практический пример: логирование файлов

# ❌ Плохо: списки
def read_large_log_file():
    lines = []  # Загрузить весь файл в память
    with open('huge.log', 'r') as f:
        for line in f:
            lines.append(line)
    return lines  # Может быть 10 GB в памяти!

# Использование
for line in read_large_log_file():
    if 'ERROR' in line:
        print(line)
        break  # Но прочитали весь файл, хотя нашли ошибку на 100-й строке!

# ✅ Хорошо: генератор
def read_large_log_file():
    with open('huge.log', 'r') as f:
        for line in f:  # Строка за строкой
            yield line

# Использование
for line in read_large_log_file():
    if 'ERROR' in line:
        print(line)
        break  # Прочитали только до первой ошибки, остальное не трогали

4. Обработка потоков данных (Streaming)

Генераторы идеальны для работы с потоками, которые можно комбинировать.

# Цепочка трансформаций данных
def read_csv_file(filename):
    with open(filename, 'r') as f:
        for line in f:
            yield line.strip()

def parse_csv_line(lines):
    headers = next(lines).split(',')
    for line in lines:
        values = line.split(',')
        yield dict(zip(headers, values))

def filter_valid_users(records):
    for record in records:
        if record['age'].isdigit() and int(record['age']) >= 18:
            yield record

def select_fields(records, fields):
    for record in records:
        yield {k: record[k] for k in fields if k in record}

# Комбинирование без промежуточных списков
pipeline = (
    read_csv_file('users.csv')  # ~100 MB
    |> parse_csv_line  # Парсинг
    |> filter_valid_users  # Фильтр
    |> select_fields(records, ['name', 'email'])  # Выбор полей
)

# Проитерировать: ОДНА строка за раз, без загрузки всего в память
for user in pipeline:
    print(user)

# Общая память: ~1 KB (одна строка), а не ~100 MB!

5. Бесконечные последовательности

Список не может быть бесконечным, генератор — может.

# Бесконечный список: невозможно
# my_list = [1, 2, 3, 4, 5, ...]  # MemoryError

# Бесконечный генератор: спокойно
def infinite_counter():
    i = 0
    while True:
        yield i
        i += 1

# Берём сколько нужно
for count in itertools.islice(infinite_counter(), 5):
    print(count)  # 0, 1, 2, 3, 4

# Генератор Фибоначчи
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib_gen = fibonacci()
first_10_fibs = list(itertools.islice(fib_gen, 10))

6. Data Engineering: обработка больших датасетов

Сценарий: обработка миллиона CSV файлов с миллионом строк каждый

# ❌ Плохо: список
def process_files_list(directory):
    all_data = []  # ❌ В памяти 1 млн x 1 млн = 1 трлн записей???
    for filename in os.listdir(directory):
        df = pd.read_csv(filename)
        all_data.append(df)
    return pd.concat(all_data)  # OOM (Out of Memory)

# ✅ Хорошо: генератор
def process_files_generator(directory):
    for filename in os.listdir(directory):
        df = pd.read_csv(filename)
        for _, row in df.iterrows():
            yield row  # Одна строка за раз

# Обработка
for row in process_files_generator('/data'):
    save_to_warehouse(row)  # Загружаем построчно
    # Память: всегда < 10 MB (одна строка)

# Альтернатива: дочанкирование
def process_files_chunked(directory, chunk_size=10000):
    chunk = []
    for filename in os.listdir(directory):
        df = pd.read_csv(filename)
        for _, row in df.iterrows():
            chunk.append(row)
            if len(chunk) >= chunk_size:
                yield chunk
                chunk = []
    if chunk:
        yield chunk

for chunk in process_files_chunked('/data'):
    bulk_insert_to_warehouse(chunk)  # Вставляем батчами

7. Generator expressions (компактная синтаксис)

# Список: скобки []
my_list = [i**2 for i in range(1000000)]

# Генератор: скобки ()
my_gen = (i**2 for i in range(1000000))

# Одно отличие, огромная разница в памяти

# Генератор в функциях
result = sum(i**2 for i in range(1000000))  # Хороший код
result = sum([i**2 for i in range(1000000)])  # Плохой код (промежуточный список)

8. Когда использовать список vs генератор

СценарийИспользуйПочему
Нужно mehrere times iterateСписокГенератор одноразовый
Нужен индекс: data[5]СписокГенератор не поддерживает индексацию
Маленький набор (< 10k)СписокПросто и быстро
Большой набор (> 1M)ГенераторПамять + скорость
Бесконечный потокГенераторСписок не может быть бесконечным
Потоковая обработкаГенераторLazy evaluation
Нужна длина len(data)СписокГенератор не знает свою длину

9. Контрольные минусы генераторов

# ❌ Генератор одноразовый
gen = (i for i in range(5))
list(gen)   # [0, 1, 2, 3, 4]
list(gen)   # [] (генератор исчерпан!)

# ❌ Нет индексации
gen = (i for i in range(5))
gen[0]  # TypeError: 'generator' object is not subscriptable

# ❌ Нет len()
gen = (i for i in range(5))
len(gen)  # TypeError: object of type 'generator' has no len()

# ✓ Если нужны эти свойства — используй список или itertools.tee()
from itertools import tee
gen1, gen2 = tee(my_generator())  # Два независимых генератора

10. Практический пример: ETL с генераторами

# Pipeline обработки 100 GB логов

def read_logs(file_path):
    """Читаем строку за строкой"""
    with gzip.open(file_path, 'rt') as f:
        for line in f:
            yield line

def parse_logs(lines):
    """Парсим JSON логи"""
    for line in lines:
        try:
            yield json.loads(line)
        except json.JSONDecodeError:
            continue  # Пропускаем плохие строки

def filter_errors(records):
    """Оставляем только ERROR логи"""
    for record in records:
        if record.get('level') == 'ERROR':
            yield record

def aggregate_by_hour(records, batch_size=10000):
    """Агрегируем по часам"""
    batch = []
    for record in records:
        batch.append(record)
        if len(batch) >= batch_size:
            yield batch
            batch = []
    if batch:
        yield batch

# Использование
pipeline = (
    read_logs('/var/log/app.log.gz')
    |> parse_logs
    |> filter_errors
    |> aggregate_by_hour
)

for batch in pipeline:
    # Каждый батч: ~10k ERROR логов
    save_to_warehouse(batch)
    # Память: ~10 MB (только батч в памяти)

# Без генераторов пришлось бы загрузить 100 GB в память!

Заключение

Генераторы — это суперсила для Data Engineer'а:

Преимущества:

  • Минимальная память (O(1) vs O(n))
  • Мгновенное создание
  • Lazy evaluation (вычисляем только нужное)
  • Ленивые цепочки трансформаций
  • Бесконечные последовательности

Недостатки:

  • Одноразовый (не iterate дважды)
  • Нет индексации и len()
  • Медленнее, чем список в цикле (но экономит память)

Rule of thumb: Если работаешь с потоками > 1 млн элементов или с файлами > 100 MB — используй генератор. В 90% случаев Data Engineering это будет правильный выбор.