Pythonгенераторыyieldосновы

Генераторы и yield в Python для аналитика

2026-07-12 10 мин

Генератор в Python — это функция, которая отдаёт значения по одному через yield, а не собирает их разом в список. Практический смысл для аналитика простой: файл на 20 ГБ или бесконечный поток событий можно обрабатывать построчно, держа в памяти один элемент вместо миллионов. Там, где df = pd.read_csv(...) на большом файле роняет ядро с MemoryError, генератор спокойно проходит те же данные за постоянную память. Дальше — как это работает, где я реально этим пользуюсь и что за это спрашивают на собеседовании.

Что такое генератор и чем он отличается от списка?

Список хранит все элементы сразу. Когда я пишу [x * x for x in range(10_000_000)], интерпретатор считает все десять миллионов квадратов и кладёт их в память — это несколько сотен мегабайт, даже если мне нужна была только сумма.

Генератор хранит рецепт, а не результат. Он помнит, на каком элементе остановился, и вычисляет следующий только когда его попросят.

# Список — все элементы в памяти сразу
squares_list = [x * x for x in range(10_000_000)]   # ~400 МБ в RAM

# Генератор — одно значение за раз
squares_gen = (x * x for x in range(10_000_000))    # несколько сотен байт

Разница видна через sys.getsizeof: список занимает мегабайты, сам объект-генератор — постоянные ~100–200 байт независимо от того, сколько элементов он отдаст. Это и есть ключевая мысль: у генератора память не растёт вместе с размером данных.

Плата за это — генератор одноразовый и не поддерживает индексацию. Нельзя взять squares_gen[500] или пройти его дважды. Об этом ниже отдельно.

Как yield превращает функцию в генератор?

Как только в теле функции появляется yield, функция перестаёт быть обычной. Её вызов больше не выполняет код — он возвращает объект-генератор. Тело начинает работать только когда я начинаю по нему итерироваться.

def read_orders(path):
    with open(path, encoding="utf-8") as f:
        next(f)  # пропускаем строку заголовка
        for line in f:
            order_id, user_id, amount = line.rstrip("\n").split(",")
            yield int(order_id), int(user_id), float(amount)
yield работает как пауза. Функция доходит до него, отдаёт значение наружу, замораживается — с сохранением всех локальных переменных и позиции в файле — и ждёт следующего запроса. При следующей итерации выполнение продолжается со строки после yield, а не с начала функции. В этом отличие от return, который завершает функцию насовсем.

Отсюда приятное следствие: with open(...) внутри генератора держит файл открытым ровно столько, сколько я его читаю. Как только итерация закончилась, файл корректно закрывается сам.

Проверить понимание паузы-возобновления удобно руками: вставьте функцию с yield в Python-песочницу, оберните вызов в next() несколько раз подряд и посмотрите, где именно она каждый раз останавливается.

Как читать большой лог-файл построчно, не загружая в память?

Сам объект файла в Python уже итератор — проход по нему отдаёт строки по одной, а не читает файл целиком. На этом строится вся обработка логов.

Первый раз я это прочувствовал, когда pd.read_csv на годовой выгрузке событий положил мне ядро Jupyter: 12 ГБ CSV в 8 ГБ памяти не лезли никак. А построчный генератор прошёл тот же файл, не заметив его размера.

Допустим, из access-лога на несколько гигабайт нужно вытащить все запросы, вернувшие 500-ю ошибку:

def server_errors(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            if ' 500 ' in line:
                yield line.rstrip("\n")

for line in server_errors("access.log"):
    print(line)

В любой момент в памяти живёт одна строка. Файл может быть больше всей оперативки машины — это не мешает.

По-настоящему генераторы раскрываются, когда я собираю из них конвейер. Каждый шаг — отдельный генератор, данные текут через них лениво, без промежуточных списков:

def read_lines(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            yield line.rstrip("\n")

def only_errors(lines):
    for line in lines:
        if "ERROR" in line:
            yield line

def to_fields(lines):
    for line in lines:
        yield line.split("\t")

lines = read_lines("app.log")
errors = only_errors(lines)
rows = to_fields(errors)

for row in rows:
    # вот здесь файл наконец начинает реально читаться
    handle(row)

Пока я не запущу финальный for, с диска не прочитается ни байта. Три генератора связаны, но не сделано ничего — это и есть ленивые вычисления. Дальше данные проходят через всю цепочку по одной строке за раз, без единого полного списка в памяти.

Чем генераторное выражение отличается от list comprehension?

Синтаксически — только скобками. Квадратные дают список, круглые — генератор.

# list comprehension — материализует весь список
amounts = [float(row[2]) for row in rows]

# генераторное выражение — ленивое, заранее не считает ничего
amounts = (float(row[2]) for row in rows)

Разница становится огромной, когда результат сразу уходит в функцию-потребитель вроде sum, max, min, any, all. Тогда список не нужен вообще:

# Плохо на больших данных: сначала собираем весь список в память
total = sum([amount for _, _, amount in read_orders("orders.csv")])

# Хорошо: значения текут по одному, память постоянная
total = sum(amount for _, _, amount in read_orders("orders.csv"))

Во втором варианте скобки генератора даже не приходится дублировать: когда генераторное выражение — единственный аргумент функции, внешние круглые скобки убираются. sum съедает числа по мере поступления и не держит ни одного лишнего.

Правило, которым я пользуюсь: если результат нужен целиком и не раз — беру список. Если он проходится один раз и уходит в агрегат или цикл — беру генератор. Больше примеров с обоими вариантами разобрано в справочнике по Python.

Как обрабатывать данные чанками, если построчно неудобно?

Построчное чтение — не единственный режим. Иногда удобнее резать данные на куски по N строк: например, чтобы считать агрегаты через pandas, но не тянуть весь файл. pd.read_csv с параметром chunksize возвращает не датафрейм, а итератор по чанкам.

import pandas as pd

revenue_by_city = {}

for chunk in pd.read_csv("events.csv", chunksize=500_000):
    grouped = chunk.groupby("city")["revenue"].sum()
    for city, rev in grouped.items():
        revenue_by_city[city] = revenue_by_city.get(city, 0) + rev

Каждый chunk — обычный датафрейм на 500 тысяч строк. Я считаю по нему частичную сумму, подмешиваю в общий словарь и отпускаю чанк — сборщик мусора освобождает память до следующей итерации. Файл на 200 млн строк так проходится на ноутбуке с 8 ГБ памяти, хотя целиком туда бы не влез.

Свой генератор чанков пишется в пару строк, если данные приходят не из pandas, а, скажем, из курсора базы или из другого генератора:

def chunks(iterable, size):
    batch = []
    for item in iterable:
        batch.append(item)
        if len(batch) == size:
            yield batch
            batch = []
    if batch:            # не забываем последний неполный кусок
        yield batch

Такой chunks я использую для батчевой вставки в БД и для отправки данных пачками в API — по 1000 записей за раз вместо строки-по-строке. Похожие задачи на обработку выгрузок разобраны в тестовых заданиях.

Почему генератор можно пройти только один раз?

Потому что он не хранит элементы — он их производит. Как только генератор дошёл до конца, внутри не осталось состояния, чтобы начать заново.

gen = (x for x in range(3))
print(list(gen))   # [0, 1, 2]
print(list(gen))   # [] — уже исчерпан

Это ловушка, на которой спотыкаются, когда генератор случайно проходят дважды. Классика — посчитать длину, а потом попытаться проитерировать те же данные:

rows = (line.split(",") for line in open("orders.csv"))
n = sum(1 for _ in rows)      # прошли генератор до конца
for row in rows:              # тут уже пусто, цикл не сделает ни одной итерации
    process(row)

Если данные реально нужны несколько раз — либо материализуйте их в список через list(...) и смиритесь с памятью, либо перечитайте источник заново, создав новый генератор. Универсального «перемотать назад» у генератора нет, и это осознанная плата за экономию. Списку такое не грозит: его можно переиспользовать сколько угодно, он же держит всё внутри. Вот и весь размен — память против переиспользования.

Что спрашивают про генераторы на собеседовании?

Вопрос про разницу между списком и генератором — почти обязательный на позициях аналитика и дата-инженера. Что хотят услышать:

Любимый практический вопрос: «как посчитать сумму по колонке в файле, который не помещается в память?». Правильный ответ — не pd.read_csv целиком, а построчный генератор или chunksize. Ещё спрашивают, что выведет двойной проход по генератору (во второй раз — пустоту): так проверяют, поняли ли вы одноразовость.

Разбор таких вопросов с формулировками ответов лежит в базе вопросов с собеседований, а потренировать код на реальных задачах можно в Python-тренажёре. Если готовитесь под конкретного работодателя — загляните на страницы компаний с реальными вопросами. А понять, на какую вилку по грейдам рассчитывать, помогает обзор зарплат аналитика — генераторы там, конечно, не главный фактор, но умение не ронять пайплайн на больших данных ценят на грейд выше.

Что запомнить

Генераторы — это не «продвинутый Python», а базовая экономия памяти, которая нужна аналитику каждый раз, когда данных больше, чем оперативки. Держите в голове три вещи: yield ставит функцию на паузу и превращает её в генератор, круглые скобки вместо квадратных дают ленивое выражение, а пройти генератор можно ровно один раз. Всё остальное — чтение логов построчно, нарезка на чанки, конвейеры из генераторов — строится на этих трёх идеях.

Дальше стоит копнуть модуль itertools (islice, chain, groupby) и связку генераторов с pandas.read_csv(chunksize=...) на своих выгрузках — там они окупаются быстрее всего.

Если хотите закрыть Python и SQL под собеседование системно, а не по кусочкам, — в плане подготовки с Pro открыты все 400+ Python-задач, тренажёр с проверкой и разборы вопросов от реальных компаний. Начать можно бесплатно.

Связанная тема — List comprehension в Python простыми словами.

Закрепи на практике
Python-задачи через pandas/numpy/scipy — попробуй бесплатно.
Python-тренажёр →