← Назад к вопросам

Как взаимодействуют между собой генератор и корутина?

2.2 Middle🔥 141 комментариев
#Python Core#Асинхронность и многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Взаимодействие генератора и корутины: от yield к async/await

Генераторы и корутины — два ключевых концепта для понимания асинхронного программирования в Python. Корутины эволюционировали из генераторов и сохраняют много общих черт, но имеют принципиально разные применения.

1. Генератор: основы

Что такое генератор?

# Функция с yield — это генератор
def simple_generator():
    print("Start")
    yield 1
    print("After 1")
    yield 2
    print("After 2")
    yield 3
    print("Done")

# Генератор не выполняется сразу
gen = simple_generator()
print(type(gen))  # <class generator>

# Вызов next() выполняет код до следующего yield
print(next(gen))   # Start | 1
print(next(gen))   # After 1 | 2
print(next(gen))   # After 2 | 3
print(next(gen))   # Done | StopIteration

Характеристики генератора:

  • Ленивое вычисление (lazy evaluation)
  • Сохраняет состояние между вызовами
  • Используется для итерации
  • yield возвращает значение и приостанавливает выполнение

Двусторонняя коммуникация через send()

def bidirectional_generator():
    print("Generator started")
    x = yield 1  # yield возвращает 1, ждёт send()
    print(f"Received: {x}")
    y = yield 2
    print(f"Received: {y}")
    yield 3

gen = bidirectional_generator()
print(next(gen))          # Generator started | 1
print(gen.send(10))       # Received: 10 | 2
print(gen.send(20))       # Received: 20 | 3
print(gen.send(30))       # StopIteration

# Вывод:
# Generator started
# 1
# Received: 10
# 2
# Received: 20
# 3

Это критично: generator может получать значения и менять своё поведение!

2. Корутина: генератор на основе yield from

yield from: делегирование

# Классический способ (Python 3.3+): generator-based coroutine
def sub_generator():
    yield 1
    yield 2
    return 3  # Значение вернётся из yield from

def delegating_generator():
    result = yield from sub_generator()
    print(f"Sub-gen returned: {result}")
    yield 4

gen = delegating_generator()
for value in gen:
    print(value)  # 1, 2, 4

# Вывод:
# 1
# 2
# Sub-gen returned: 3
# 4

Ключевое различие:

  • yield передаёт значение и ждёт next()
  • yield from полностью делегирует управление другому генератору
  • Корутина может вызывать другие корутины через yield from

3. Эволюция: async/await (современные корутины)

Native Coroutines vs Generator-based

# ❌ Generator-based coroutine (устаревает, Python 3.3-3.9)
import asyncio

@asyncio.coroutine  # Deprecated
def old_style_coro():
    yield from asyncio.sleep(1)
    return "Done"

# ✅ Native coroutine (Python 3.5+, рекомендуется)
async def modern_coro():
    await asyncio.sleep(1)
    return "Done"

# Синтаксис различен, но идея похожа

async/await расширяет концепцию генераторов

import asyncio

async def fetch_data(url: str):
    print(f"Fetching {url}")
    await asyncio.sleep(2)  # Симуляция HTTP запроса
    print(f"Done {url}")
    return f"Data from {url}"

# async function — это корутина
coro = fetch_data("http://example.com")
print(type(coro))  # <class coroutine>

# asyncio.run() выполняет корутину (как event loop)
result = asyncio.run(fetch_data("http://example.com"))
print(result)

# Вывод:
# Fetching http://example.com
# Done http://example.com
# Data from http://example.com

4. Глубокая взаимосвязь: что происходит под капотом

Генератор в Event Loop (как это работает)

# Event loop управляет корутинами как если бы это были генераторы

import asyncio

async def task1():
    print("Task 1 start")
    await asyncio.sleep(0.5)
    print("Task 1 end")
    return "Result 1"

async def task2():
    print("Task 2 start")
    await asyncio.sleep(0.3)
    print("Task 2 end")
    return "Result 2"

async def main():
    # Запускаем обе задачи параллельно
    results = await asyncio.gather(task1(), task2())
    print(results)

asyncio.run(main())

# Вывод (заметь порядок):
# Task 1 start
# Task 2 start
# Task 2 end      <- task2 завершилась первой (0.3s)
# Task 1 end      <- task1 завершилась второй (0.5s)
# ["Result 1", "Result 2"]

Как это работает:

  1. Event loop создаёт две корутины
  2. Запускает task1(), пока не встретит await
  3. Встретил await asyncio.sleep(0.5) — приостанавливает task1
  4. Запускает task2(), пока не встретит await
  5. Встретил await asyncio.sleep(0.3) — приостанавливает task2
  6. Ждёт, когда один из таймеров закончится
  7. task2 закончилась (0.3s < 0.5s) — возобновляет task2
  8. task2 завершилась
  9. task1 закончилась (0.5s) — возобновляет task1
  10. task1 завершилась

Визуализация: где yield и await?

# Под капотом asyncio обрабатывает корутины как генераторы

class EventLoop:
    def __init__(self):
        self.tasks = []  # Очередь задач
        self.timers = {}  # Таймеры
    
    def run_until_complete(self, coro):
        task = Task(coro)
        self.tasks.append(task)
        
        while self.tasks or self.timers:
            # Выполняем задачу
            task = self.tasks.pop(0)
            try:
                # Корутина — это генератор
                # next(coro) == await что-то
                result = next(task.coro)
                
                # Если это таймер, добавляем в очередь
                if isinstance(result, Timer):
                    self.timers[task] = result
                else:
                    self.tasks.append(task)
            except StopIteration as e:
                # Корутина завершилась
                task.result = e.value

# На практике asyncio делает это сложнее, но идея та же

5. Практические примеры взаимодействия

Пример 1: Последовательное выполнение

import asyncio

async def step1():
    print("Step 1: начало")
    await asyncio.sleep(1)
    print("Step 1: конец")
    return "Result 1"

async def step2(prev_result):
    print(f"Step 2: получил {prev_result}")
    await asyncio.sleep(1)
    print("Step 2: конец")
    return "Result 2"

async def sequential():
    # ❌ Неправильно: параллельно
    # await asyncio.gather(step1(), step2(None))  # 2 секунды
    
    # ✅ Правильно: последовательно
    r1 = await step1()
    r2 = await step2(r1)
    return r1, r2

asyncio.run(sequential())

# Вывод:
# Step 1: начало
# Step 1: конец
# Step 2: получил Result 1
# Step 2: конец

Пример 2: Обработка множества запросов

import asyncio
import aiohttp  # Асинхронный HTTP клиент

async def fetch_url(session, url):
    async with session.get(url) as response:
        return await response.json()

async def fetch_multiple(urls):
    # session — асинхронный контекстный менеджер
    async with aiohttp.ClientSession() as session:
        # Запускаем все запросы параллельно
        tasks = [fetch_url(session, url) for url in urls]
        results = await asyncio.gather(*tasks)
        return results

# Использование
urls = [
    "https://api.github.com/users/guido",
    "https://api.github.com/users/gvanrossum",
]
results = asyncio.run(fetch_multiple(urls))

Пример 3: Генератор как источник данных для корутины

async def process_data():
    # Генератор создаёт данные
    def data_generator():
        for i in range(5):
            yield i * 2
    
    # Корутина обрабатывает данные
    for value in data_generator():
        print(f"Processing: {value}")
        await asyncio.sleep(0.5)  # Имитация обработки
        print(f"Done: {value}")

asyncio.run(process_data())

# Вывод:
# Processing: 0
# Done: 0
# Processing: 2
# Done: 2
# Processing: 4
# Done: 4

6. Таблица: Генератор vs Корутина vs async/await

АспектГенераторКорутина (yield from)Async/Await
Синтаксисyieldyield fromasync/await
ИспользованиеИтерация, ленивостьАсинхронный код (старый стиль)Асинхронный код (рекомендуется)
Управлениеnext(), send()next(), send() (опосредованно)Event loop
Двусторонняя коммуникацияДа (send())ДаДа (return в await)
Может ждать другую?Нет (только iterate)Да (yield from)Да (await)
ИсключенияМожно бросать через throw()Можно бросатьМожно бросать (try/except)
ПроизводительностьВысокая (CPU-bound)Средняя (I/O-bound)Высокая (I/O-bound)

7. Ключевые принципы взаимодействия

# 1. Корутина — это более сложный генератор
def is_generator(obj):
    import types
    return isinstance(obj, types.GeneratorType)

def is_coroutine(obj):
    import asyncio
    return asyncio.iscoroutine(obj)

# Генератор
gen = (x for x in range(3))
print(is_generator(gen))   # True
print(is_coroutine(gen))   # False

# Корутина
async def coro():
    return 1

print(is_generator(coro()))  # False
print(is_coroutine(coro()))  # True

# 2. Event loop требует корутины (или futures, tasks)
# 3. Генератор может быть обёрнут в корутину
# 4. await ждёт только coroutine/future/task
# 5. yield может вернуть любое значение

Резюме

Генератор:

  • Функция с yield
  • Ленивое вычисление через итерацию
  • Может общаться с caller через send()

Корутина:

  • Генератор на основе yield from
  • Может вызывать другие корутины
  • Управляется event loop

Async/Await:

  • Синтаксический сахар для корутин (Python 3.5+)
  • await вместо yield from
  • Event loop автоматически управляет параллелизмом

Ключевая идея: Асинхронное программирование в Python основано на том же принципе, что и генераторы — приостановка и возобновление выполнения. Event loop управляет множеством корутин, как if они были генераторами в памяти.