UpPetto

This is a real course generated by UpPetto — unedited.

AI-generated. Two models wrote it, a third checked every risky claim against sources, a fourth re-checked.

Create my own course

Генераторы и generator expressions вместо ручных буферов

core заменить функцию, которая накапливает весь результат в списке, на генератор, вычисляющий значения по требованию

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

Первая и самая важная тонкость для C-программиста: вызов функции с yield внутри не выполняет её тело. Он возвращает объект-генератор — «заготовку», которая начнёт выполняться только при первом обращении (next() или начале for). Это разительно отличается от C, где вызов функции немедленно исполняет её код до return.

def squares_up_to(n):
    print("старт")
    for i in range(n):
        yield i * i

g = squares_up_to(5)   # ничего не напечаталось — тело ещё не выполнялось
print(next(g))          # только теперь печатается "старт", и возвращается 0

Вторая тонкость: генератор — одноразовый итератор. Он не хранит все значения, как список, который можно перечитать сколько угодно раз; после исчерпания повторный обход даёт пустую последовательность, а когда значения кончаются, итератор сигнализирует об этом исключением StopIteration (цикл for перехватывает его сам).

sequenceDiagram
    participant Caller as Вызывающий код
    participant L as Жадная функция (список)
    participant G as Генератор (yield)
    Caller->>L: get_squares(1000000)
    L->>L: сразу вычисляет все элементы
    L-->>Caller: возвращает готовый список в памяти
    Caller->>G: get_squares_gen(1000000)
    G-->>Caller: возвращает объект-генератор, тело не выполнено
    Caller->>G: next()
    G-->>Caller: одно вычисленное значение
    Caller->>G: next()
    G-->>Caller: следующее значение по требованию
Жадное вычисление списка целиком против ленивой выдачи значений генератором
sequenceDiagram
    participant Client as Клиент (цикл for)
    participant GenFunc as generate_squares()
    participant GenObj as Объект-генератор

    Client->>GenFunc: generate_squares(3)
    GenFunc-->>GenObj: создать
    GenObj-->>Client: объект-генератор
    Client->>GenObj: next()
    Note over GenObj: код до yield, i=0
    GenObj-->>Client: 0
    Client->>GenObj: next()
    Note over GenObj: возобновление, i=1
    GenObj-->>Client: 1
    Client->>GenObj: next()
    Note over GenObj: возобновление, i=2
    GenObj-->>Client: 4
    Client->>GenObj: next()
    Note over GenObj: цикл завершён, выход из функции
    GenObj-->>Client: StopIteration
Жизненный цикл генератора: создание, ленивая выдача значений и завершение

Практическая ценность для C-программиста — замена ручного буфера. В C, обрабатывая большой или потенциально неограниченный поток (файл, сокет, датчик), вы либо ограничиваете буфер фиксированным размером, либо пишете realloc-логику. В Python достаточно не накапливать результат в списке, а выдавать его по одному значению через yield — расход памяти остаётся почти постоянным независимо от размера источника.

Частые ошибки переноса привычек из C:

  1. Функция всегда собирает полный список и возвращает его, даже когда вызывающий код проходит по нему один раз и выбрасывает — признак «не доверяю языку, соберу буфер сам».
  2. Реализация генераторного поведения через класс с ручным __next__ и полями состояния, когда yield решает задачу без явного класса.
  3. Забыть, что генератор — не список: передать его в функцию, которая хочет пройти по данным дважды или вызвать len() — на генераторе len() не работает, а повторный проход даёт пустоту.

Кроме функций с yield, есть генераторные выражения — то же, что comprehension, но в круглых скобках и без построения коллекции в памяти:

squares_list = [i * i for i in range(10**7)]  # жадно: список из 10 млн элементов
squares_gen  = (i * i for i in range(10**7))  # лениво: лёгкий объект-генератор
total = sum(x * x for x in range(1_000_000))  # ни один список не строится целиком

Квадратные скобки строят весь список сразу, круглые — отдают значения по одному; для промежуточных трансформаций почти всегда нужны круглые.

Разобранный пример

Функция читает большой лог-файл, парсит строки и возвращает список результатов — типичный «C-буфер», перенесённый в Python:

def parse_errors(path):
    result = []
    with open(path) as f:
        for line in f:
            if "ERROR" in line:
                result.append(line.strip())
    return result

Если файл на несколько гигабайт, а вызывающему коду нужна первая сотня ошибок, память уходит на построение полного списка. Ленивая версия:

def parse_errors(path):
    with open(path) as f:
        for line in f:
            if "ERROR" in line:
                yield line.strip()

Файловый объект сам является ленивым итератором по строкам, поэтому for err in parse_errors(path): ... обрабатывает файл построчно, и break после сотой ошибки реально останавливает чтение, а не просто выходит из цикла по готовому списку.

Попробуй сейчас

Возьмите функцию:

def first_n_primes(n):
    result = []
    candidate = 2
    while len(result) < n:
        if all(candidate % p != 0 for p in result):
            result.append(candidate)
        candidate += 1
    return result

Перепишите её как генератор, который отдаёт простые числа бесконечно (без параметра n), а количество нужных чисел пусть контролирует вызывающий код через itertools.islice или обычный break в цикле.

Получилось, если…

Вы справились, если ваша функция не принимает n вообще, использует yield candidate внутри бесконечного while True, и вызов next(gen) пять раз подряд корректно даёт первые пять простых чисел без предварительного вычисления остальных. Вы также поняли, что генератор — это фабрика итераторов, способная моделировать бесконечные последовательности при почти постоянном расходе памяти.

Вывод

yield заменяет ручной конечный автомат с буфером на функцию, которая сама помнит, где остановилась; вызов такой функции не выполняет тело сразу, а генератор истощается после одного прохода — это не список, а одноразовый источник значений по требованию. Для промежуточных вычислений используйте генераторные выражения (expr for ...) вместо [expr for ...].

AI-generated · source-grounded review

🛡 Fact-checked: 2 risky claims verified · 1 removed · confidence: high · figures: 2 · a second model checked these claims and agreed
[verified] Вызов функции с yield не исполняет её тело, а возвращает объект-генератор; генератор одноразовый
База: генераторы — ленивые итераторы, вычисляющие значения по требованию с сохранением состояния
[verified] Фигуры (mermaid): жадный список против ленивого генератора; жизненный цикл yield 0,1,4 и затем StopIteration
Значения соответствуют i*i для range(3); ленивая семантика подтверждена базой
[softened] Генератор использует постоянный объём памяти независимо от размера источника
База говорит о ленивом вычислении без хранения всего результата; формулировка смягчена до 'почти постоянного' расхода
[removed] Формулировка 'память используется constant' заменена на 'расход памяти остаётся почти постоянным' — точное утверждение о строго постоянной памяти не подтверждено базой
Key concepts: yield generator expression lazy evaluation
Tell me more 🔒 Didn't understand — explain simply 🔒 Show examples 🔒 Sources 🔒

On your own course these buttons answer instantly, quizzes track what you've mastered, and lessons adapt to your gaps. Write my course

Check yourself

1. Что возвращает вызов функции с yield внутри в момент самого вызова?
2. В чём отличие `(x*x for x in range(10))` от `[x*x for x in range(10)]`?
3. Главное отличие генератора от списка при повторном использовании:
On your own course, these are marked as you answer