Изучите делегирующие генераторы

PythonPythonBeginner
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

В этом практическом занятии (лабораторной работе) вы узнаете о делегировании генераторов в Python с использованием оператора yield from. Эта возможность, введенная в Python 3.3, упрощает код, который зависит от генераторов и корутин.

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

Цели:

  • Понимать назначение оператора yield from
  • Уметь использовать yield from для делегирования другим генераторам
  • Применять эти знания для упрощения кода на основе корутин
  • Понимать связь с современным синтаксисом async/await

Файлы, с которыми вы будете работать:

  • cofollow.py - Содержит вспомогательные функции для корутин
  • server.py - Содержит простую реализацию сетевого сервера

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python(("Python")) -.-> python/AdvancedTopicsGroup(["Advanced Topics"]) python(("Python")) -.-> python/NetworkingGroup(["Networking"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python/FunctionsGroup -.-> python/function_definition("Function Definition") python/ObjectOrientedProgrammingGroup -.-> python/encapsulation("Encapsulation") python/AdvancedTopicsGroup -.-> python/generators("Generators") python/AdvancedTopicsGroup -.-> python/threading_multiprocessing("Multithreading and Multiprocessing") python/NetworkingGroup -.-> python/socket_programming("Socket Programming") subgraph Lab Skills python/function_definition -.-> lab-132527{{"Изучите делегирующие генераторы"}} python/encapsulation -.-> lab-132527{{"Изучите делегирующие генераторы"}} python/generators -.-> lab-132527{{"Изучите делегирующие генераторы"}} python/threading_multiprocessing -.-> lab-132527{{"Изучите делегирующие генераторы"}} python/socket_programming -.-> lab-132527{{"Изучите делегирующие генераторы"}} end

Понимание оператора yield from

На этом этапе мы рассмотрим оператор yield from в Python. Этот оператор представляет собой мощный инструмент при работе с генераторами и упрощает процесс делегирования операций другим генераторам. К концу этого этапа вы поймете, что такое yield from, как он работает и как он может обрабатывать передачу значений между разными генераторами.

Что такое yield from?

Оператор yield from был введен в Python 3.3. Его основная цель - упростить делегирование операций подгенераторам. Подгенератор - это просто другой генератор, которому основной генератор может делегировать работу.

Обычно, когда вы хотите, чтобы генератор возвращал значения из другого генератора, вам приходится использовать цикл. Например, без yield from вы бы написали код следующим образом:

def delegating_generator():
    for value in subgenerator():
        yield value

В этом коде delegating_generator использует цикл for для перебора значений, генерируемых subgenerator, и затем возвращает каждое значение по очереди.

Однако с использованием оператора yield from код становится намного проще:

def delegating_generator():
    yield from subgenerator()

Эта одна строка кода дает тот же результат, что и цикл в предыдущем примере. Но yield from не просто сокращенная запись. Он также управляет двусторонней связью между вызывающим кодом и подгенератором. Это означает, что любые значения, отправленные в делегирующий генератор, передаются непосредственно в подгенератор.

Простой пример

Создадим простой пример, чтобы увидеть, как работает yield from на практике.

  1. Сначала нам нужно открыть файл cofollow.py в редакторе. Для этого мы используем команду cd для перехода в правильную директорию. Выполните следующую команду в терминале:
cd /home/labex/project
  1. Затем мы добавим две функции в файл cofollow.py. Функция subgen - это простой генератор, который возвращает числа от 0 до 4. Функция main_gen использует yield from для делегирования генерации этих чисел subgen и затем возвращает строку 'Done'. Добавьте следующий код в конец файла cofollow.py:
def subgen():
    for i in range(5):
        yield i

def main_gen():
    yield from subgen()
    yield 'Done'
  1. Теперь протестируем эти функции. Откройте оболочку Python и выполните следующий код:
from cofollow import subgen, main_gen

## Test subgen directly
for x in subgen():
    print(x)

## Test main_gen that delegates to subgen
for x in main_gen():
    print(x)

При выполнении этого кода вы должны увидеть следующий вывод:

0
1
2
3
4

0
1
2
3
4
Done

Этот вывод показывает, что yield from позволяет main_gen передавать все значения, сгенерированные subgen, непосредственно вызывающему коду.

Передача значений с использованием yield from

Одна из самых мощных возможностей yield from - это его способность обрабатывать передачу значений в обоих направлениях. Создадим более сложный пример, чтобы продемонстрировать это.

  1. Добавьте следующие функции в файл cofollow.py:
def accumulator():
    total = 0
    while True:
        value = yield total
        if value is None:
            break
        total += value

def caller():
    acc = accumulator()
    yield from acc
    yield 'Total accumulated'

Функция accumulator - это корутина, которая отслеживает текущую сумму. Она возвращает текущую сумму и затем ожидает получения нового значения. Если она получает None, то останавливает цикл. Функция caller создает экземпляр accumulator и использует yield from для делегирования всех операций отправки и приема значений ему.

  1. Протестируйте эти функции в оболочке Python:
from cofollow import caller

c = caller()
print(next(c))  ## Start the coroutine
print(c.send(1))  ## Send value 1, get accumulated value
print(c.send(2))  ## Send value 2, get accumulated value
print(c.send(3))  ## Send value 3, get accumulated value
print(c.send(None))  ## Send None to exit the accumulator

При выполнении этого кода вы должны увидеть следующий вывод:

0
1
3
6
'Total accumulated'

Этот вывод показывает, что yield from полностью делегирует все операции отправки и приема значений подгенератору до тех пор, пока он не исчерпает все значения.

Теперь, когда вы понимаете основы yield from, мы перейдем к более практическим применениям на следующем этапе.

✨ Проверить решение и практиковаться

Использование yield from в корутинах

На этом этапе мы рассмотрим, как использовать оператор yield from с корутинами для более практических применений. Корутины - это мощная концепция в Python, и понимание того, как использовать yield from с ними, может значительно упростить ваш код.

Корутины и передача сообщений

Корутины - это специальные функции, которые могут получать значения через оператор yield. Они чрезвычайно полезны для таких задач, как обработка данных и обработка событий. В файле cofollow.py есть декоратор consumer. Этот декоратор помогает настроить корутины, автоматически продвигая их до первой точки yield. Это означает, что вам не нужно вручную запускать корутину; декоратор делает это за вас.

Создадим корутину, которая получает значения и проверяет их типы. Вот как вы можете это сделать:

  1. Сначала откройте файл cofollow.py в редакторе. Вы можете использовать следующую команду в терминале, чтобы перейти в правильную директорию:
cd /home/labex/project
  1. Затем добавьте следующую функцию receive в конец файла cofollow.py. Эта функция представляет собой корутину, которая будет получать сообщение и проверять его тип.
def receive(expected_type):
    """
    A coroutine that receives a message and validates its type.
    Returns the received message if it matches the expected type.
    """
    msg = yield
    assert isinstance(msg, expected_type), f'Expected type {expected_type}'
    return msg

Вот что делает эта функция:

  • Она использует yield без выражения для получения значения. Когда в корутину отправляют значение, этот оператор yield захватывает его.
  • Она проверяет, является ли полученное значение ожидаемого типа, используя функцию isinstance. Если типы не совпадают, она вызывает исключение AssertionError.
  • Если проверка типа проходит успешно, она возвращает значение.
  1. Теперь создадим корутину, которая использует yield from с нашей функцией receive. Эта новая корутина будет получать и выводить только целые числа.
@consumer
def print_ints():
    """
    A coroutine that receives and prints integers only.
    Uses yield from to delegate to the receive coroutine.
    """
    while True:
        val = yield from receive(int)
        print('Got:', val)
  1. Чтобы протестировать эту корутину, откройте оболочку Python и выполните следующий код:
from cofollow import print_ints

p = print_ints()
p.send(42)
p.send(13)
try:
    p.send('13')  ## This should raise an AssertionError
except AssertionError as e:
    print(f"Error: {e}")

Вы должны увидеть следующий вывод:

Got: 42
Got: 13
Error: Expected type <class 'int'>

Понимание работы yield from с корутинами

Когда мы используем yield from receive(int) в корутине print_ints, происходят следующие шаги:

  1. Управление делегируется корутине receive. Это означает, что корутина print_ints приостанавливается, и начинает выполняться корутина receive.
  2. Корутина receive использует yield для получения значения. Она ожидает, пока в нее отправят значение.
  3. Когда в print_ints отправляют значение, на самом деле его получает receive. Оператор yield from заботится о передаче значения из print_ints в receive.
  4. Корутина receive проверяет тип полученного значения. Если тип правильный, она возвращает значение.
  5. Возвращенное значение становится результатом выражения yield from в корутине print_ints. Это означает, что переменной val в print_ints присваивается значение, возвращаемое receive.

Использование yield from делает код более читаемым, чем если бы мы должны были напрямую обрабатывать выдачу и получение значений. Он абстрагирует сложность передачи значений между корутинами.

Создание более сложных корутин для проверки типов

Расширим наши вспомогательные функции, чтобы они могли обрабатывать более сложную проверку типов. Вот как вы можете это сделать:

  1. Добавьте следующие функции в файл cofollow.py:
def receive_dict():
    """Receive and validate a dictionary"""
    result = yield from receive(dict)
    return result

def receive_str():
    """Receive and validate a string"""
    result = yield from receive(str)
    return result

@consumer
def process_data():
    """Process different types of data using the receive utilities"""
    while True:
        print("Waiting for a string...")
        name = yield from receive_str()
        print(f"Got string: {name}")

        print("Waiting for a dictionary...")
        data = yield from receive_dict()
        print(f"Got dictionary with {len(data)} items: {data}")

        print("Processing complete for this round.")
  1. Чтобы протестировать новую корутину, откройте оболочку Python и выполните следующий код:
from cofollow import process_data

proc = process_data()
proc.send("John Doe")
proc.send({"age": 30, "city": "New York"})
proc.send("Jane Smith")
try:
    proc.send(123)  ## This should raise an AssertionError
except AssertionError as e:
    print(f"Error: {e}")

Вы должны увидеть такой вывод:

Waiting for a string...
Got string: John Doe
Waiting for a dictionary...
Got dictionary with 2 items: {'age': 30, 'city': 'New York'}
Processing complete for this round.
Waiting for a string...
Got string: Jane Smith
Waiting for a dictionary...
Error: Expected type <class 'dict'>

Оператор yield from делает код чище и более читаемым. Он позволяет нам сосредоточиться на высокоуровневой логике нашей программы, а не тратить время на детали передачи сообщений между корутинами.

✨ Проверить решение и практиковаться

Оборачивание сокетов с использованием генераторов

На этом этапе мы узнаем, как использовать генераторы для оборачивания операций с сокетами. Это очень важная концепция, особенно в контексте асинхронного программирования. Асинхронное программирование позволяет вашему приложению обрабатывать несколько задач одновременно, не дожидаясь завершения одной задачи перед началом другой. Использование генераторов для оборачивания операций с сокетами делает код более эффективным и легким в управлении.

Понимание проблемы

В файле server.py содержится простая реализация сетевого сервера с использованием генераторов. Давайте посмотрим на текущий код. Этот код является основой нашего сервера, и понимание его важно перед внесением каких-либо изменений.

def tcp_server(address, handler):
    sock = socket(AF_INET, SOCK_STREAM)
    sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    sock.bind(address)
    sock.listen(5)
    while True:
        yield 'recv', sock
        client, addr = sock.accept()
        tasks.append(handler(client, addr))

def echo_handler(client, address):
    print('Connection from', address)
    while True:
        yield 'recv', client
        data = client.recv(1000)
        if not data:
            break
        yield 'send', client
        client.send(b'GOT:' + data)
    print('Connection closed')
    client.close()

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

Создание класса GenSocket

Давайте создадим класс GenSocket для оборачивания операций с сокетами с использованием генераторов. Это сделает наш код чище и более читаемым. Инкапсулируя операции с сокетами в класс, мы можем скрыть детали цикла событий от пользователя и сосредоточиться на высокоуровневой логике сервера.

  1. Откройте файл server.py в редакторе:
cd /home/labex/project

Эта команда изменяет текущую директорию на директорию проекта, где находится файл server.py. После перехода в правильную директорию вы можете открыть файл в предпочитаемом текстовом редакторе.

  1. Добавьте следующий класс GenSocket в конец файла, перед любыми существующими функциями:
class GenSocket:
    """
    A generator-based wrapper for socket operations.
    """
    def __init__(self, sock):
        self.sock = sock

    def accept(self):
        """Accept a connection and return a new GenSocket"""
        yield 'recv', self.sock
        client, addr = self.sock.accept()
        return GenSocket(client), addr

    def recv(self, maxsize):
        """Receive data from the socket"""
        yield 'recv', self.sock
        return self.sock.recv(maxsize)

    def send(self, data):
        """Send data to the socket"""
        yield 'send', self.sock
        return self.sock.send(data)

    def __getattr__(self, name):
        """Forward any other attributes to the underlying socket"""
        return getattr(self.sock, name)

Класс GenSocket действует как обертка для операций с сокетами. Метод __init__ инициализирует класс объектом сокета. Методы accept, recv и send выполняют соответствующие операции с сокетами и используют yield для указания, когда операция готова. Метод __getattr__ позволяет классу передавать любые другие атрибуты базовому объекту сокета.

  1. Теперь измените функции tcp_server и echo_handler для использования класса GenSocket:
def tcp_server(address, handler):
    sock = GenSocket(socket(AF_INET, SOCK_STREAM))
    sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    sock.bind(address)
    sock.listen(5)
    while True:
        client, addr = yield from sock.accept()
        tasks.append(handler(client, addr))

def echo_handler(client, address):
    print('Connection from', address)
    while True:
        data = yield from client.recv(1000)
        if not data:
            break
        yield from client.send(b'GOT:' + data)
    print('Connection closed')
    client.close()

Обратите внимание, как явные операторы yield 'recv', sock и yield 'send', client были заменены более чистым выражением yield from. Ключевое слово yield from используется для делегирования выполнения другому генератору. Это делает код более читаемым и скрывает детали цикла событий от пользователя. Теперь код выглядит больше как обычные вызовы функций, и пользователю не нужно беспокоиться о внутреннем устройстве цикла событий.

  1. Давайте добавим простую тестовую функцию, чтобы продемонстрировать, как будет использоваться наш сервер:
def run_server():
    """Start the server on port 25000"""
    tasks.append(tcp_server(('localhost', 25000), echo_handler))
    try:
        event_loop()
    except KeyboardInterrupt:
        print("Server stopped")

if __name__ == '__main__':
    print("Starting echo server on port 25000...")
    print("Press Ctrl+C to stop")
    run_server()

Этот код более читаемый и поддерживаемый. Класс GenSocket инкапсулирует логику использования yield, позволяя серверному коду сосредоточиться на высокоуровневом потоке выполнения, а не на деталях цикла событий. Функция run_server запускает сервер на порту 25000 и обрабатывает исключение KeyboardInterrupt, которое позволяет пользователю остановить сервер, нажав Ctrl+C.

Понимание преимуществ

Подход с использованием yield from имеет несколько преимуществ:

  1. Чистый код: Операции с сокетами выглядят больше как обычные вызовы функций. Это делает код легче читать и понимать, особенно для начинающих.
  2. Абстракция: Детали цикла событий скрыты от пользователя. Пользователю не нужно знать, как работает цикл событий, чтобы использовать серверный код.
  3. Читаемость: Код лучше выражает, что он делает, а не как он это делает. Это делает код более самодокументированным и легким в поддержке.
  4. Поддерживаемость: Изменения в цикле событий не потребуют изменений в серверном коде. Это означает, что если вам нужно изменить цикл событий в будущем, вы можете сделать это, не влияя на серверный код.

Этот паттерн является ступенью к современному синтаксису async/await, который мы рассмотрим на следующем этапе. Синтаксис async/await - это более продвинутый и чистый способ написания асинхронного кода в Python, и понимание паттерна yield from поможет вам легче перейти к нему.

✨ Проверить решение и практиковаться

От генераторов к async/await

На этом последнем этапе мы рассмотрим, как паттерн yield from в Python превратился в современный синтаксис async/await. Понимание этого эволюционного процесса крайне важно, так как оно помогает увидеть связь между генераторами и асинхронным программированием. Асинхронное программирование позволяет вашему приложению обрабатывать несколько задач одновременно, не дожидаясь завершения каждой из них, что особенно полезно в сетевом программировании и других операциях, ограниченных вводом-выводом (I/O).

Связь между генераторами и async/await

Синтаксис async/await, введенный в Python 3.5, основан на функциональности генераторов и оператора yield from. По сути, async-функции реализуются с использованием генераторов. Это означает, что концепции, которые вы узнали о генераторах, напрямую связаны с тем, как работает async/await.

Для перехода от использования генераторов к синтаксису async/await нам нужно выполнить следующие шаги:

  1. Использовать декоратор @coroutine из модуля types. Этот декоратор помогает преобразовать функции, основанные на генераторах, в форму, которая может использоваться с async/await.
  2. Преобразовать функции, использующие yield from, на использование async и await вместо этого. Это делает код более читаемым и лучше выражает асинхронную природу операций.
  3. Обновить цикл событий для обработки нативных корутин. Цикл событий отвечает за планирование и выполнение асинхронных задач.

Обновление класса GenSocket

Теперь давайте модифицируем наш класс GenSocket для работы с декоратором @coroutine. Это позволит нашему классу использоваться в контексте async/await.

  1. Откройте файл server.py в редакторе. Вы можете сделать это, выполнив следующую команду в терминале:
cd /home/labex/project
  1. В начале файла server.py добавьте импорт coroutine. Этот импорт необходим для использования декоратора @coroutine.
from types import coroutine
  1. Обновите класс GenSocket для использования декоратора @coroutine. Этот декоратор преобразует наши методы, основанные на генераторах, в ожидаемые корутины, что означает, что они могут использоваться с ключевым словом await.
class GenSocket:
    """
    A generator-based wrapper for socket operations
    that works with async/await.
    """
    def __init__(self, sock):
        self.sock = sock

    @coroutine
    def accept(self):
        """Accept a connection and return a new GenSocket"""
        yield 'recv', self.sock
        client, addr = self.sock.accept()
        return GenSocket(client), addr

    @coroutine
    def recv(self, maxsize):
        """Receive data from the socket"""
        yield 'recv', self.sock
        return self.sock.recv(maxsize)

    @coroutine
    def send(self, data):
        """Send data to the socket"""
        yield 'send', self.sock
        return self.sock.send(data)

    def __getattr__(self, name):
        """Forward any other attributes to the underlying socket"""
        return getattr(self.sock, name)

Преобразование к синтаксису async/await

Далее, давайте преобразуем наш серверный код для использования синтаксиса async/await. Это сделает код более читаемым и ясно выражает асинхронную природу операций.

async def tcp_server(address, handler):
    """
    An asynchronous TCP server using async/await.
    """
    sock = GenSocket(socket(AF_INET, SOCK_STREAM))
    sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    sock.bind(address)
    sock.listen(5)
    while True:
        client, addr = await sock.accept()
        tasks.append(handler(client, addr))

async def echo_handler(client, address):
    """
    An asynchronous handler for echo clients.
    """
    print('Connection from', address)
    while True:
        data = await client.recv(1000)
        if not data:
            break
        await client.send(b'GOT:' + data)
    print('Connection closed')
    client.close()

Обратите внимание, что yield from заменено на await, и функции теперь определяются с помощью async def вместо def. Это изменение делает код более интуитивно понятным и легким для понимания.

Понимание преобразования

Переход от генераторов с yield from к синтаксису async/await не является просто простым синтаксическим изменением. Он представляет собой сдвиг в том, как мы думаем о асинхронном программировании.

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

    • При использовании генераторов с yield from вы явно передаете управление, чтобы сигнализировать, что задача готова. Это означает, что вы должны вручную управлять тем, когда задача может продолжиться.
    • Вам также нужно вручную управлять планированием задач. Это может быть сложно, особенно в более крупных программах.
    • Основное внимание уделяется механике управления потоком выполнения, что может сделать код труднее для чтения и поддержки.
  2. Синтаксис async/await:

    • С синтаксисом async/await управление неявно передается в точках await. Это делает код более простым, так как вам не нужно беспокоиться о явном передаче управления.
    • Цикл событий заботится о планировании задач, поэтому вам не нужно управлять этим вручную.
    • Основное внимание уделяется логическому потоку программы, что делает код более читаемым и поддерживаемым.

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

Современное асинхронное программирование

В современном Python обычно используется модуль asyncio для асинхронного программирования вместо собственного цикла событий. Модуль asyncio предоставляет встроенную поддержку многих полезных функций:

  • Запуск нескольких корутин одновременно. Это позволяет вашему приложению обрабатывать несколько задач одновременно.
  • Управление сетевым вводом-выводом. Он упрощает процесс отправки и получения данных по сети.
  • Примитивы синхронизации. Они помогают управлять доступом к общими ресурсам в многопоточном окружении.
  • Планирование и отмену задач. Вы можете легко запланировать выполнение задач в определенное время и отменить их при необходимости.

Вот как может выглядеть наш сервер с использованием asyncio:

import asyncio

async def handle_client(reader, writer):
    addr = writer.get_extra_info('peername')
    print(f'Connection from {addr}')

    while True:
        data = await reader.read(1000)
        if not data:
            break

        writer.write(b'GOT:' + data)
        await writer.drain()

    print('Connection closed')
    writer.close()
    await writer.wait_closed()

async def main():
    server = await asyncio.start_server(
        handle_client, 'localhost', 25000
    )

    addr = server.sockets[0].getsockname()
    print(f'Serving on {addr}')

    async with server:
        await server.serve_forever()

if __name__ == '__main__':
    asyncio.run(main())

Этот код реализует ту же функциональность, что и наш сервер на основе генераторов, но использует стандартную библиотеку asyncio, которая более надежная и богатая по функционалу.

Заключение

В этом практическом занятии вы узнали о нескольких важных концепциях:

  1. Оператор yield from и то, как он делегирует выполнение другому генератору. Это фундаментальная концепция для понимания работы генераторов.
  2. Как использовать yield from с корутинами для передачи сообщений. Это позволяет вам обмениваться данными между различными частями вашего асинхронного приложения.
  3. Оборачивание операций с сокетами с использованием генераторов для более чистого кода. Это делает ваш сетевой код более организованным и легким для понимания.
  4. Переход от генераторов к современному синтаксису async/await. Понимание этого перехода поможет вам писать более читаемый и поддерживаемый асинхронный код на Python, будь то использование генераторов напрямую или современного синтаксиса async/await.
✨ Проверить решение и практиковаться

Резюме

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

Эти концепции являются важными для понимания асинхронного программирования в Python. Переход от генераторов к современному синтаксису async/await представляет собой значительное продвижение в обработке асинхронных операций. Чтобы более углубленно изучить эти концепции, вы можете изучить модуль asyncio, рассмотреть, как популярные фреймворки используют async/await, и разработать собственные асинхронные библиотеки. Понимание делегирующих генераторов и оператора yield from дает более глубокое понимание подхода Python к асинхронному программированию.