Создание и применение генераторов в Python: от базовых конструкций до сложных сценариев

Раздел: Продвинутый Python -> Использование генераторов

Использование генераторов в Python

Генераторы позволяют создавать итераторы без явной реализации методов __iter__ и __next__. В основе лежит ключевое слово yield, которое приостанавливает выполнение функции, сохраняя её состояние. Такой подход экономит память и упрощает код.

Базовый генератор с yield

Как создать генератор, возвращающий последовательность чисел?

Определим функцию с yield внутри цикла. При каждом вызове next() выполнение возобновляется до следующего yield.


def countdown(n):
    while n > 0:
        yield n
        n -= 1

# Использование
for i in countdown(5):
    print(i)

генератор python (генераторы в python)

5
4
3
2
1

Типичная ошибка: забыть обработать StopIteration.

Генератор автоматически вызывает исключение StopIteration, когда больше нет значений. Если вручную вызывать next() после окончания, возникнет ошибка. Используйте цикл for или обрабатывайте исключение.


gen = countdown(3)
print(next(gen))  # 3
print(next(gen))  # 2
print(next(gen))  # 1
print(next(gen))  # StopIteration

Как создать генератор в одну строку с помощью генераторного выражения?

Генераторное выражение похоже на списковое включение, но использует круглые скобки. Оно возвращает объект-генератор, а не готовый список.


squares = (x**2 for x in range(10))
print(type(squares))  # <class 'generator'>
print(list(squares))  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Проблема: генераторное выражение можно использовать только один раз.

После исчерпания генератор становится пустым. Если нужно повторять, сохраните результат в список или создайте новый генератор.


gen = (x for x in [1,2,3])
print(list(gen))  # [1,2,3]
print(list(gen))  # []

Как передавать значения обратно в генератор с помощью send?

Метод send() позволяет отправить значение внутрь генератора, которое станет результатом выражения yield. Это часто используется для реализации конечных автоматов.


def accumulator():
    total = 0
    while True:
        value = yield total
        if value is None:
            break
        total += value

acc = accumulator()
next(acc)  # запускаем до первого yield
print(acc.send(10))  # 10
print(acc.send(5))   # 15
acc.send(None)       # StopIteration (завершение)

Ошибка: вызов send без предварительного вызова next.

Перед первой отправкой значения необходимо «разогреть» генератор, инициализировав его с помощью next(gen) или gen.send(None). Иначе возникнет TypeError: can't send non-None value to a just-started generator.

Как создать бесконечный генератор, избегая переполнения памяти?

Бесконечные генераторы удобны для потоковой обработки данных. Они не хранят все элементы, а выдают их по мере необходимости.


def infinite_sequence():
    num = 0
    while True:
        yield num
        num += 1

# Взять первые 5 элементов
gen = infinite_sequence()
for _ in range(5):
    print(next(gen))  # 0,1,2,3,4

Риск: бесконечный цикл, если не задать условие выхода.

При использовании for без break программа зависнет. Всегда предусматривайте ограничение (например, itertools.islice).


from itertools import islice
print(list(islice(infinite_sequence(), 3)))  # [0,1,2]

Как делегировать генерацию другому генератору с помощью yield from?

Ключевое слово yield from позволяет генератору частично или полностью передать управление другому итератору. Удобно при рекурсивных структурах (деревья, директории).


def internal_gen():
    yield 'внутренний 1'
    yield 'внутренний 2'

def outer_gen():
    yield 'внешний старт'
    yield from internal_gen()
    yield 'внешний конец'

print(list(outer_gen()))
# ['внешний старт', 'внутренний 1', 'внутренний 2', 'внешний конец']

Проблема: ошибки внутри дочернего генератора не обрабатываются автоматически.

Если внутренний генератор выбрасывает исключение, оно распространяется наружу. Можно обернуть yield from в try/except, чтобы управлять потоком ошибок.


def faulty():
    yield 'до'
    raise ValueError('Ошибка!')

def safe():
    try:
        yield from faulty()
    except ValueError as e:
        yield f'Перехвачено: {e}'

print(list(safe()))  # ['до', 'Перехвачено: Ошибка!']

Дополнительные примеры использования генераторов

Здесь представлены расширенные сценарии, которые демонстрируют гибкость генераторов в Python.

Пример

# Пример 1: Обход дерева в глубину (DFS) с помощью yield from
class TreeNode:
    def __init__(self, value, children=None):
        self.value = value
        self.children = children or []

def dfs(node):
    yield node.value
    for child in node.children:
        yield from dfs(child)

# Строим дерево:  A -> B -> D, E
#                     -> C -> F
tree = TreeNode('A', [
    TreeNode('B', [TreeNode('D'), TreeNode('E')]),
    TreeNode('C', [TreeNode('F')])
])
print('DFS обход:', list(dfs(tree)))
# Результат: ['A', 'B', 'D', 'E', 'C', 'F']
Пример

# Пример 2: Pipeline генераторов для обработки данных
# Чтение строк, фильтрация, преобразование

def read_lines(filename):
    with open(filename, 'r') as f:
        for line in f:
            yield line.rstrip('\n')

def filter_condition(lines, substring):
    for line in lines:
        if substring in line:
            yield line

def capitalize(lines):
    for line in lines:
        yield line.upper()

# Допустим, файл data.txt содержит строки
# generator pipeline не требует загрузки всего файла
# pipeline = capitalize(filter_condition(read_lines('data.txt'), 'python'))
# for line in pipeline:
#     print(line)
Пример

# Пример 3: Генератор для чтения больших файлов блоками

def read_chunks(file_path, chunk_size=1024):
    with open(file_path, 'rb') as f:
        while True:
            chunk = f.read(chunk_size)
            if not chunk:
                break
            yield chunk

# Использование:
# for chunk in read_chunks('bigfile.bin', 4096):
#     process(chunk)

# Этот подход предотвращает загрузку всего файла в память.
Пример

# Пример 4: Комбинация нескольких итераторов с itertools.chain
from itertools import chain

def gen_a():
    yield 'a1'
    yield 'a2'

def gen_b():
    yield 'b1'
    yield 'b2'

combined = chain(gen_a(), gen_b())
print('Объединённые генераторы:', list(combined))
# Результат: ['a1', 'a2', 'b1', 'b2']
Пример

# Пример 5: Генератор с close() и обработкой исключений генератора

def controlled():
    try:
        while True:
            received = yield
            print(f'Получено: {received}')
    except GeneratorExit:
        print('Генератор закрыт')

gen = controlled()
next(gen)
gen.send('Значение')
gen.close()
# Вывод: Получено: Значение
#        Генератор закрыт
Пример

# Пример 6: Генератор как источник корутин (asyncio не рассматриваем)
# Использование throw() для передачи исключения внутрь генератора

def custom_except():
    while True:
        try:
            val = yield
            print(f'Обработано: {val}')
        except ValueError:
            print('Поймано ValueError, продолжаем')

gen = custom_except()
next(gen)
gen.send(10)          # Обработано: 10
gen.throw(ValueError) # Поймано ValueError, продолжаем
gen.close()
Пример

# Пример 7: Генератор для последовательности Фибоначчи с ограничением по памяти

def fibonacci(limit=None):
    a, b = 0, 1
    while limit is None or a < limit:
        yield a
        a, b = b, a + b

print('Фибоначчи до 100:', list(fibonacci(100)))
# Результат: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

Генераторы в Python - comments

En
генератор python (python)