Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Внутри CPython гид по интерпретатору Python.pdf
Скачиваний:
6
Добавлен:
07.04.2024
Размер:
8.59 Mб
Скачать

Сопрограммы    249

Если StopAsyncIteration было выдано явно, и это асинхронный генератор, то выдается RuntimeError, так как такая ситуация недопустима.

15. Наконец, результат возвращается вызывающей стороне __next__().

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

СОПРОГРАММЫ

У сопрограмм есть одно серьезное ограничение: они могут возвращать значения вызовом yield только своей непосредственной стороне вызова.

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

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

cpython-book-samples 33 letter_coroutines.py

def gen_letters(start, x): i = start

end = start + x while i < end: yield chr(i)

i += 1

def letters(upper): if upper:

yield from gen_letters(65, 26) # A--Z else:

yield from gen_letters(97, 26) # a--z

for letter in letters(False):

# Нижний регистр a--z print(letter)

Книги для программистов: https://t.me/booksforits

250    Параллелизм и конкурентность

for letter in letters(True):

# Верхний регистр A--Z print(letter)

Генераторы также отлично подходят для ленивых последовательностей (lazy sequences1), в которых они могут вызываться многократно.

Концепция сопрограммы (coroutine), развивающая поведение генераторов (например, возможность приостановки и возобновления выполнения), была применена в Python во многих API.

Генераторы представляют собой ограниченную форму сопрограммы, потому что генераторам можно отправлять данные методом .send(). Возможна двусторонняя отправка сообщений между стороной вызова и целью. Сопрограммы также хранят сторону вызова в атрибуте cr_origin.

Изначально сопрограммы включались декоратором, но этот вариант считается устаревшим и был заменен «полноценными» сопрограммами с использованием ключевых слов async и await.

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

Для создания сопрограммы следует определить функцию с ключевыми словами async def. В этом примере добавляется таймер с использованием функции asyncio.sleep() и возвращается строка активизации:

>>>import asyncio

>>>async def sleepy_alarm(time):

... await asyncio.sleep(time)

... return "wake up!"

>>>alarm = sleepy_alarm(10)

>>>alarm

<coroutine object sleepy_alarm at 0x1041de340>

При вызове функция возвращает объект сопрограммы.

Существует много способов выполнения сопрограммы. Самый простой — использование функции asyncio.run(coro). Выполните asyncio.run() со своим объектом сопрограммы, и через 10 секунд будет выдан сигнал:

1Вычисление элемента последовательности откладывается, пока не потребуется результат. — Примеч. ред.

Книги для программистов: https://t.me/booksforits

Сопрограммы    251

>>> asyncio.run(alarm) 'wake up'

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

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

API задач используется для планирования и выполнения нескольких сопрограмм одновременно. Перед планированием задач должен быть запущен цикл событий. Цель цикла событий — планирование одновременно выполняемых задач и связывание событий (завершение, отмена, исключения и т. д.) с обратными вызовами.

Когда мы вызывали asyncio.run() (из Lib asyncio runners.py), функция выполняла за нас следующие задачи:

1.Запуск нового цикла событий.

2.Упаковка объекта сопрограммы в задачу.

3.Назначение обратного вызова для завершения задачи.

4.Цикл по задаче до ее завершения.

5.Возвращение результата.

Исходные файлы

Ниже указан исходный файл, относящийся к сопрограммам.

ФАЙЛ НАЗНАЧЕНИЕ

Lib asyncio

Реализация asyncio из стандартной библиотеки Python

Циклы событий

Циклы событий связывают асинхронный код воедино. Они пишутся на чистом Python и представляют собой объекты, содержащие задачи.

Книги для программистов: https://t.me/booksforits

252    Параллелизм и конкурентность

Все задачи в цикле могут иметь обратные вызовы. Цикл выполняет обратные вызовы при завершении задачи или сбое:

loop = asyncio.new_event_loop()

Внутри цикла находится последовательность задач, представленная типом asyncio.Task. Задачи планируются в цикле, а затем, когда этот цикл запущен, он перебирает все задачи, пока они не будут завершены.

Вы можете преобразовать один таймер в цикл задач:

cpython-book-samples 33 sleepy_alarm.py import asyncio

async def sleepy_alarm(person, time): await asyncio.sleep(time) print(f"{person} -- wake up!")

async def wake_up_gang(): tasks = [

asyncio.create_task(sleepy_alarm("Bob", 3), name="wake up Bob"), asyncio.create_task(sleepy_alarm("Yudi", 4), name="wake up Yudi"), asyncio.create_task(sleepy_alarm("Doris", 2), name="wake up Doris"), asyncio.create_task(sleepy_alarm("Kim", 5), name="wake up Kim")

]

await asyncio.gather(*tasks)

asyncio.run(wake_up_gang())

Программа выводит следующий результат:

Doris -- wake up!

Bob -- wake up!

Yudi -- wake up!

Kim -- wake up!

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

Цикл событий выполняет объекты сопрограмм sleepy_alarm() снова и снова, пока await asyncio.sleep() не вернет конечный результат, и функция print() сможет выполниться.

Чтобы эта схема работала, необходимо использовать asyncio.sleep() вместо блокирующего (и не async-совместимого) вызова time.sleep().

Книги для программистов: https://t.me/booksforits

Сопрограммы    253

Пример

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

zz Измените check_port() для подключения через сокет, а не через функцию asyncio.open_connection(), создающую future-объект вместо немедленного подключения.

zz Используйте future-подключение через сокет в событии таймера с asyncio.wait_for().

zz Добавьте порт в список результатов (results), если проверка была успешной.

zz Добавьте новую функцию scan() для создания сопрограмм check_port() для каждого порта. Добавьте их в список задач tasks.

zz Объедините все задачи в новую сопрограмму вызовом asyncio.gather(). zz Проведите сканирование вызовом asyncio.run().

Код выглядит так:

cpython-book-samples 33 portscanner_async.py

import time import asyncio

timeout = 1.0

async def check_port(host: str, port: int, results: list): try:

future = asyncio.open_connection(host=host, port=port) r, w = await asyncio.wait_for(future, timeout=timeout) results.append(port)

w.close()

except OSError: # Ничего не делаем при закрытии порта pass

except asyncio.TimeoutError:

pass # Порт закрыт, пропустить и продолжить

async def scan(start, end, host): tasks = []

results = []

for port in range(start, end): tasks.append(check_port(host, port, results))

await asyncio.gather(*tasks) return results

if __name__ == '__main__':

Книги для программистов: https://t.me/booksforits