Введение
В этом практическом занятии (лабораторной работе) вы узнаете о делегировании генераторов в Python с использованием оператора yield from. Эта возможность, введенная в Python 3.3, упрощает код, который зависит от генераторов и корутин.
Генераторы - это специальные функции, которые могут приостанавливать и возобновлять выполнение, сохраняя свое состояние между вызовами. Оператор yield from предоставляет элегантный способ делегирования управления другому генератору, улучшая читаемость и поддерживаемость кода.
Цели:
- Понимать назначение оператора
yield from - Уметь использовать
yield fromдля делегирования другим генераторам - Применять эти знания для упрощения кода на основе корутин
- Понимать связь с современным синтаксисом async/await
Файлы, с которыми вы будете работать:
cofollow.py- Содержит вспомогательные функции для корутинserver.py- Содержит простую реализацию сетевого сервера
Понимание оператора 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 на практике.
- Сначала нам нужно открыть файл
cofollow.pyв редакторе. Для этого мы используем командуcdдля перехода в правильную директорию. Выполните следующую команду в терминале:
cd /home/labex/project
- Затем мы добавим две функции в файл
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'
- Теперь протестируем эти функции. Откройте оболочку 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 - это его способность обрабатывать передачу значений в обоих направлениях. Создадим более сложный пример, чтобы продемонстрировать это.
- Добавьте следующие функции в файл
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 для делегирования всех операций отправки и приема значений ему.
- Протестируйте эти функции в оболочке 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. Это означает, что вам не нужно вручную запускать корутину; декоратор делает это за вас.
Создадим корутину, которая получает значения и проверяет их типы. Вот как вы можете это сделать:
- Сначала откройте файл
cofollow.pyв редакторе. Вы можете использовать следующую команду в терминале, чтобы перейти в правильную директорию:
cd /home/labex/project
- Затем добавьте следующую функцию
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. - Если проверка типа проходит успешно, она возвращает значение.
- Теперь создадим корутину, которая использует
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)
- Чтобы протестировать эту корутину, откройте оболочку 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, происходят следующие шаги:
- Управление делегируется корутине
receive. Это означает, что корутинаprint_intsприостанавливается, и начинает выполняться корутинаreceive. - Корутина
receiveиспользуетyieldдля получения значения. Она ожидает, пока в нее отправят значение. - Когда в
print_intsотправляют значение, на самом деле его получаетreceive. Операторyield fromзаботится о передаче значения изprint_intsвreceive. - Корутина
receiveпроверяет тип полученного значения. Если тип правильный, она возвращает значение. - Возвращенное значение становится результатом выражения
yield fromв корутинеprint_ints. Это означает, что переменнойvalвprint_intsприсваивается значение, возвращаемоеreceive.
Использование yield from делает код более читаемым, чем если бы мы должны были напрямую обрабатывать выдачу и получение значений. Он абстрагирует сложность передачи значений между корутинами.
Создание более сложных корутин для проверки типов
Расширим наши вспомогательные функции, чтобы они могли обрабатывать более сложную проверку типов. Вот как вы можете это сделать:
- Добавьте следующие функции в файл
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.")
- Чтобы протестировать новую корутину, откройте оболочку 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 для оборачивания операций с сокетами с использованием генераторов. Это сделает наш код чище и более читаемым. Инкапсулируя операции с сокетами в класс, мы можем скрыть детали цикла событий от пользователя и сосредоточиться на высокоуровневой логике сервера.
- Откройте файл
server.pyв редакторе:
cd /home/labex/project
Эта команда изменяет текущую директорию на директорию проекта, где находится файл server.py. После перехода в правильную директорию вы можете открыть файл в предпочитаемом текстовом редакторе.
- Добавьте следующий класс
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__ позволяет классу передавать любые другие атрибуты базовому объекту сокета.
- Теперь измените функции
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 используется для делегирования выполнения другому генератору. Это делает код более читаемым и скрывает детали цикла событий от пользователя. Теперь код выглядит больше как обычные вызовы функций, и пользователю не нужно беспокоиться о внутреннем устройстве цикла событий.
- Давайте добавим простую тестовую функцию, чтобы продемонстрировать, как будет использоваться наш сервер:
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 имеет несколько преимуществ:
- Чистый код: Операции с сокетами выглядят больше как обычные вызовы функций. Это делает код легче читать и понимать, особенно для начинающих.
- Абстракция: Детали цикла событий скрыты от пользователя. Пользователю не нужно знать, как работает цикл событий, чтобы использовать серверный код.
- Читаемость: Код лучше выражает, что он делает, а не как он это делает. Это делает код более самодокументированным и легким в поддержке.
- Поддерживаемость: Изменения в цикле событий не потребуют изменений в серверном коде. Это означает, что если вам нужно изменить цикл событий в будущем, вы можете сделать это, не влияя на серверный код.
Этот паттерн является ступенью к современному синтаксису 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 нам нужно выполнить следующие шаги:
- Использовать декоратор
@coroutineиз модуляtypes. Этот декоратор помогает преобразовать функции, основанные на генераторах, в форму, которая может использоваться сasync/await. - Преобразовать функции, использующие
yield from, на использованиеasyncиawaitвместо этого. Это делает код более читаемым и лучше выражает асинхронную природу операций. - Обновить цикл событий для обработки нативных корутин. Цикл событий отвечает за планирование и выполнение асинхронных задач.
Обновление класса GenSocket
Теперь давайте модифицируем наш класс GenSocket для работы с декоратором @coroutine. Это позволит нашему классу использоваться в контексте async/await.
- Откройте файл
server.pyв редакторе. Вы можете сделать это, выполнив следующую команду в терминале:
cd /home/labex/project
- В начале файла
server.pyдобавьте импортcoroutine. Этот импорт необходим для использования декоратора@coroutine.
from types import coroutine
- Обновите класс
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 не является просто простым синтаксическим изменением. Он представляет собой сдвиг в том, как мы думаем о асинхронном программировании.
Генераторы с yield from:
- При использовании генераторов с
yield fromвы явно передаете управление, чтобы сигнализировать, что задача готова. Это означает, что вы должны вручную управлять тем, когда задача может продолжиться. - Вам также нужно вручную управлять планированием задач. Это может быть сложно, особенно в более крупных программах.
- Основное внимание уделяется механике управления потоком выполнения, что может сделать код труднее для чтения и поддержки.
- При использовании генераторов с
Синтаксис 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, которая более надежная и богатая по функционалу.
Заключение
В этом практическом занятии вы узнали о нескольких важных концепциях:
- Оператор
yield fromи то, как он делегирует выполнение другому генератору. Это фундаментальная концепция для понимания работы генераторов. - Как использовать
yield fromс корутинами для передачи сообщений. Это позволяет вам обмениваться данными между различными частями вашего асинхронного приложения. - Оборачивание операций с сокетами с использованием генераторов для более чистого кода. Это делает ваш сетевой код более организованным и легким для понимания.
- Переход от генераторов к современному синтаксису
async/await. Понимание этого перехода поможет вам писать более читаемый и поддерживаемый асинхронный код на Python, будь то использование генераторов напрямую или современного синтаксисаasync/await.
Резюме
В этом практическом занятии вы узнали о концепции делегирующих генераторов в Python, с акцентом на оператор yield from и его различные применения. Вы изучили, как использовать yield from для делегирования выполнения другому генератору, что упрощает код и повышает его читаемость. Вы также узнали о создании корутин с использованием yield from для приема и проверки сообщений, а также о применении генераторов для оборачивания операций с сокетами для создания более чистого сетевого кода.
Эти концепции являются важными для понимания асинхронного программирования в Python. Переход от генераторов к современному синтаксису async/await представляет собой значительное продвижение в обработке асинхронных операций. Чтобы более углубленно изучить эти концепции, вы можете изучить модуль asyncio, рассмотреть, как популярные фреймворки используют async/await, и разработать собственные асинхронные библиотеки. Понимание делегирующих генераторов и оператора yield from дает более глубокое понимание подхода Python к асинхронному программированию.