Урок курса
Генераторы и yield в Python: как это работает
Python для профессионаловГенератор в Python выдаёт значения по одному, не создавая всю последовательность заранее. Обычная функция заканчивает работу через return, а функция с yield приостанавливается: локальные переменные и позиция выполнения сохраняются до следующего вызова next().
def squares(limit: int):
for number in range(limit):
yield number ** 2
values = squares(3)
print(next(values)) # 0
print(next(values)) # 1
print(list(values)) # [4]
Вызов squares(3) не выполняет тело функции целиком. Он возвращает объект-генератор. Каждый next() продолжает код до следующего yield; после исчерпания функции возникает StopIteration. Цикл for обрабатывает этот протокол автоматически.
Зачем нужна ленивость
Список хранит все элементы одновременно, а генератор — текущее состояние и очередное значение. Поэтому генераторы полезны для больших файлов, потоков событий и цепочек преобразований, когда результат можно обрабатывать последовательно.
def non_empty(lines):
for line in lines:
cleaned = line.strip()
if cleaned:
yield cleaned
with open("events.log", encoding="utf-8") as source:
for event in non_empty(source):
process(event)
Память здесь почти не зависит от размера файла. Но ленивость меняет контракт: генератор обычно одноразовый, не имеет длины и не поддерживает произвольный доступ по индексу. Если данные нужно обходить несколько раз, сортировать или быстро брать по индексу, список часто понятнее.
Yield, return и генераторные выражения
return value внутри генератора завершает итерацию; значение попадает в StopIteration.value, но обычный for его не показывает. Для передачи последовательности из другого итератора используют yield from iterable.
Генераторное выражение выглядит как comprehension в круглых скобках: (x * x for x in numbers). Оно тоже вычисляет элементы лениво. Не превращайте генераторы в самоцель: короткий список из десяти элементов проще прочитать и отладить как список.
Практическое правило: выбирайте генератор, когда источник потенциально велик или бесконечен и потребитель читает значения последовательно. Выбирайте материализованную коллекцию, когда важны повторный обход, длина, сортировка или индексация.
Ленивое выполнение и исчерпание генератора
Вызов функции-генератора создаёт объект, но не выполняет её тело до первого next. Дальше выполнение продолжается с места остановки. После исчерпания тот же объект не начинает вычисление заново — для второго прохода создают новый генератор.
def squares(n):
for i in range(n):
yield i*i
g = squares(4)
assert next(g) == 0
assert list(g) == [1,4,9]
assert list(g) == []
assert list(squares(4)) == [0,1,4,9]
print("Генератор исчерпан; новый вызов создаёт новый проход")
Попробуйте сначала посчитать sum(g), а затем передать g в список: второй потребитель ничего не получит. Это частая ошибка при повторном использовании iterator. Ленивость экономит хранение всех результатов, но не убирает стоимость вычислений и память исходного источника. Если нужен случайный доступ и многократные проходы, список может быть подходящим решением.
Продолжите разбор: Контекстный менеджер и оператор with в Python.
Задача 1
Что происходит при выполнении yield внутри функции-генератора?
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
