Генераторы

Последнее обновление: 13.09.2026

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

Генераторы часто используются как более эффективный с точки зрения использования памяти способ перебора значений в большом блоке данных, например, в файле, базе данных или по сети. При использовании yield вместо return вместо значения возвращаются объекты генераторов.

Чтобы понять разницу, посмотрим на два подхода к созданию списка чисел. Обычная функция на Python:

def usual_function():
    return [1, 2, 3]

И аналогичная функция-генератор:

def generator_function():
    yield 1
    yield 2
    yield 3

Обычная функция создает в памяти весь список [1, 2, 3] и отдает его целиком. Если в списке будет миллиард элементов, программа займет всю оперативную память и может зависнуть. А генератор не создает список. Он возвращает специальный объект-генератор. Каждый раз, когда вы просите у него следующее число, он выполняет код до следующего yield, отдает значение и снова засыпает.

Используем наш генератор. Чтобы получать из него значения, используется встроенная функция next():

# определение функции генератора
def generator_function():
    yield 1
    yield 2
    yield 3

# определяем объект генератора
gen = generator_function()

# получаем данные из генератора
print(next(gen))  # 1
print(next(gen))  # 2
print(next(gen))  # 3

Здесь функция генератора генерирует три числа, и основной части программы мы получаем все эти три числа. Но если вызвать next(gen) 4-й раз, возникнет исключение StopIteration, так как элементы закончились.

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

# определение функции генератора
def generator_function():
    yield 1
    yield 2
    yield 3

# определяем объект генератора
gen = generator_function()

# получаем данные из генератора
for number in generator_function():
    print(number)

Генераторы незаменимы, когда нужно обработать последовательность данных, но мы не знаем, сколько их будет, или их слишком много. Например, создадим генератор, который выдает алфавитные символы ASCII:

# определение функции генератора
def letters():
    i = 97  # символ 'a' в ASCII
    end = 97 + 26   # код последнего алфавитного символа ACII - 'z'
    while i < end:
        yield chr(i)    # отдаем очередной символ
        i += 1

# получаем данные из генератора
for letter in letters():
    print(letter)

Выражения генераторов

Если необходимоо создать простой генератор, писать целую функцию с yield необязательно. В Python есть выражения генераторов или генераторные выражения (generator expressions). Они пишутся так же, как выражения list comprehensions, но вместо квадратных скобок используются круглые. Например, следующий код применяет List Comprehension - он создает готовый список квадратов чисел в памяти:

squares_list = [x**2 for x in range(1000000)]

Теперь возьмем генератор (Generator Expression), который будет вычислять квадраты из аналогичного диапазона значений на ходу:

squares_gen = (x**2 for x in range(1000000))

print(next(squares_gen))  # 0
print(next(squares_gen))  # 1
print(next(squares_gen))  # 4
print(next(squares_gen))  # 9

Плюсы генераторов

  • Экономия памяти. Элементы не хранятся в ОЗУ, а создаются по требованию.

  • Быстрый старт. Функция отдает первый элемент мгновенно, не дожидаясь генерации остальных.

  • Удобство работы с потоками данных. Идеально для чтения огромных файлов или логов построчно.

Минусы генераторов:

  • Одноразовость. Пройтись по генератору циклом можно только один раз. Чтобы повторить, нужно создавать его заново.

  • Нельзя узнать длину. Функция len() с генераторами не работает.

  • Нет доступа по индексу. Нельзя написать gen[5], нужно перебрать все элементы до пятого.

Таким образом, генераторы обычно применяются при работе с большими объемами данных (например, читаете тяжелый файл построчно) или когда вам не нужно хранить всю коллекцию в памяти одновременно. Если же вам нужно постоянно возвращаться к элементам, сортировать их или искать по индексам - лучше использовать обычные списки.

Пример генератора: чтение больших файлов

Рассмотрим практический пример применения генераторов - xтение больших файлов (например, логов сервера на несколько гигабайт). Это классическая задача, где генераторы спасают оперативную память от переполнения. Если мы попытаемся прочитать тяжелый файл обычным методом readlines(), Python попытается загрузить весь файл в память целиком в виде списка строк. Если файл весит 10 ГБ, а у вас 8 ГБ оперативной памяти, программа просто упадет с ошибкой MemoryError. Генератор же будет читать файл строка за строкой. В памяти всегда будет находиться только одна текущая строка.

Представим, что у нас есть огромный файл server_logs.txt. Нам нужно найти в нем только те строки, которые содержат критическую ошибку "[ERROR]". Для этого мы можем определить следующий скрипт:

def read_file(file_path):
    # Генератор, который читает файл построчно и не забивает память
    with open(file_path, "r", encoding="utf-8") as file:
        for line in file:
            # yield отдает строку и ставит функцию на паузу,
            # пока вызывающий код не попросит следующую строку
            yield line.strip()


file_path = "server_logs.txt"

# Применяем генератор
log_generator = read_file(file_path)

for log_line in log_generator:
    if "[ERROR]" in log_line:
        print(f"Найдена ошибка: {log_line}")

Как это работает под капотом:

  1. Конструкция with open(...) открывает файл.

  2. Цикл for line in file в Python уже сам по себе является генератором (итератором). Он считывает данные с диска буферизированно, небольшими порциями.

  3. Наш yield line.strip() возвращает очищенную от лишних пробелов строку в основной цикл for log_line in log_generator.

  4. Как только строка обработана, Python выгружает ее из памяти и берет следующую.

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

import random

def generate_file(file_path, total_lines=1_000_000):
    
    # Варианты логов для генерации
    log_templates = [
        "[INFO] User logged in successfully.",
        "[INFO] Page loaded in 24ms.",
        "[WARNING] Database connection slow.",
        "[INFO] API request completed.",
        "[ERROR] Failed to connect to database!",
        "[ERROR] User authentication timeout."
    ]
    
    print(f"Начинаю генерацию файла {file_path}...")
    
    # Открываем файл для записи ("w")
    with open(file_path, "w", encoding="utf-8") as file:
        for i in range(total_lines):
            # Случайно выбираем один шаблон
            log_entry = random.choice(log_templates)
            # Записываем строку, добавляя индекс и перенос строки \n
            file.write(f"Line {i+1}: {log_entry}\n")
            
    print(f"Готово! Файл {file_path} успешно создан.")

file_path = "server_logs.txt"

# Запуск генератора файлов
#generate_file(file_path, total_lines=1_000_000)

Этот скрипт создаст текстовый файл ровно на 1 000 000 строк (около 50-70 МБ). Чтобы файл был похож на настоящий лог сервера, мы случайным образом чередуем обычные сообщения ([INFO], [WARNING]) и те самые ошибки ([ERROR]), которые наш генератор из прошлого шага должен будет найти.

Для случайного выбора строк мы используем стандартный модуль random. Обратите внимание: для записи мы тоже можем использовать концепцию генерации "на ходу", чтобы не собирать миллион строк в один гигантский список перед записью.

Конвейер из генераторов (Pipeline)

Генераторы можно связывать в цепочки. Это позволяет делать сложную фильтрацию данных, вообще не сохраняя промежуточные результаты в памяти. Для примера сделаем то же самое, но с помощью компактных генераторных выражений:

lines = (line for line in open(file_path, encoding="utf-8"))
errors = (line.strip() for line in lines if "[ERROR]" in line)
for error_line in errors: print(error_line)
  1. Первый генератор просто читает строки

    lines = (line for line in open(file_path, encoding="utf-8"))
  2. Второй генератор фильтрует и оставляет только ошибки (он берет данные из первого)

    errors = (line.strip() for line in lines if "[ERROR]" in line)
  3. Запуск конвейера: данные начинают двигаться по цепочке только здесь

    for error_line in errors: print(error_line)

Таким образом, в этом примере файл даже не начнет читаться, пока мы не запустим финальный цикл for error_line in errors. Данные будут протаскиваться через эту цепочку "лениво", по одной строке.

Асинхронные генераторы

Асинхронные генераторы по сути объединяют две концепции: генераторы и корутины с асинхронностью. Если функция объявлена с ключевым словом async и содержит оператор yield, то она преобразуется в объект асинхронного генератора при вызове.

Асинхронные генераторы во многом по использованию похожи на обычные генераторы за некоторыми исключениями. Прежде всего, для получения данных из асинхронных генераторов используется метод anext() (а не next() как у обычных генераторов). И поскольку метод anext() является асинхронным, то для его вызова применяются соответствующие инструменты для вызова асинхронных функций, например, функция asyncio.run() или оператор await. Например:

import asyncio

# определение асинхронного генератора
async def generate():
    yield 1
    yield 4
    yield 9
    yield 16

async def consume():
    # определяем объект генератора
    gen = generate()

    # получаем данные из асинхронного генератора
    print(await anext(gen))  # 1
    print(await anext(gen))  # 4
    print(await anext(gen))  # 9

asyncio.run(consume())

Другой важный момент - обычный цикл for не поймет асинхронный генератор, поэтому вместо него используется асинхронный оператор for - async for:

import asyncio

# определение функции генератора
async def generate():
    yield 1
    yield 4
    yield 9
    yield 16

async def consume():
    # получаем данные из асинхронного генератора
    async for n in generate(): 
        print(n)

asyncio.run(consume())

# Консольный вывод
# 1
# 4
# 9
# 16
Помощь сайту
Юмани:
410011174743222
Номер карты:
4048415020898850