Короткий ответ
Обычная функция с return list вычисляет и сразу хранит в памяти весь результат, а функция с yield приостанавливает выполнение на каждом значении и возвращает управление вызывающему коду, сохраняя своё состояние между вызовами next() — это позволяет обрабатывать данные потоково, не загружая всё в память одновременно.
Как это работает подробнее
Любая функция, содержащая yield, при вызове не выполняется сразу, а возвращает объект-генератор. Выполнение тела начинается только при первом next()/итерации и останавливается на каждом yield, запоминая локальные переменные и точку выполнения до следующего next(). Это и есть ленивое вычисление — значения считаются по одному, по требованию.
Пример кода
example.py
def read_large_file(path):
with open(path, "r", encoding="utf-8") as f:
for line in f: # по одной строке, не весь файл в памяти
yield line.strip()
def filter_errors(lines):
for line in lines:
if "ERROR" in line:
yield line
# Конвейер: файл -> построчный генератор -> фильтр ошибок, без промежуточных списков
# for error_line in filter_errors(read_large_file("app.log")):
# print(error_line)
gen = (x * x for x in range(5))
print(next(gen), next(gen)) # 0 1 — вычисляется по одному значениюЧто использовать на практике
- Практика. Генераторы удобны для обработки файлов построчно, постраничной выгрузки из БД, построения конвейеров трансформации данных (генератор из генератора), где важно не держать весь набор данных в памяти. Их минус — по генератору можно пройти только один раз, и нельзя получить len() или произвольный доступ по индексу без материализации в список.
Подводные камни
- Риски. Пытаются повторно использовать исчерпанный генератор или получить его длину через
len()— это не поддерживается. Также не осознают, что при построении цепочки генераторов ошибка в коде-потребителе может "молча" остановить обработку на середине, если не предусмотрена явная обработка исключений внутри цикла.