Генераторы и 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:
- Функция всегда собирает полный список и возвращает его, даже когда вызывающий код проходит по нему один раз и выбрасывает — признак «не доверяю языку, соберу буфер сам».
- Реализация генераторного поведения через класс с ручным
__next__и полями состояния, когдаyieldрешает задачу без явного класса. - Забыть, что генератор — не список: передать его в функцию, которая хочет пройти по данным дважды или вызвать
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
On your own course these buttons answer instantly, quizzes track what you've mastered, and lessons adapt to your gaps. Write my course