Для того, что бы понять, как работают сопрограммы и невытесняющая многозадачность в python, нужно пройти путь от итераторов до синтаксической конструкции языка yield from и далее к async/await.
Все мы часто сталкивались с такими последовательностями как списки. Наверняка многие слышали о шаблоне проектирования “итератор”. Так вот в python объект list является итерируемым объектом, а паттерн итератор реализован в языке с использованием протокола - определённого соглашения для программистов. Дадим более строгие определения этим понятиям.
Итерируемый объект - это объект, который реализует один или оба метода __iter__ или __getitem__. Последовательности всегда реализуют __getitem__ поэтому являются итерируемыми. Итератор можно получить, вызвав функцию iter, передав ей итерируемый объект. Пример итерируемого объекта, итератора и его обхода:
if __name__ == '__main__':
# Итерируемый объект list
sequence = [1, 2, 3, 4, 5, 6]
# Если вызвать next(sequence), то получим TypeError: 'list' object is not an iterator,
# поэтому получаем итератор из итерируемого объекта
it = iter(sequence)
while 1:
try:
element = next(it)
print(element)
except StopIteration:
break
1
2
3
4
5
6
Итератор - это объект, который реализует методы __next__ и __iter__. Т.к. протокол итератора включат в себя функцию __iter__, то итератор является итерируемым объектом, но не оборот. Ещё одним отличием этих двух сущностей в том, что пройдя по итератору один раз, его следует снова инициализировать, вызвав функцию iter. Для итерируемых коллекций, например, списков, это делать не надо. Пример реализации протокола итератора и его обхода:
class Iterator:
""" Реализация протокола итератора """
def __init__(self, sequence):
self.sequence = sequence
self.index = 0
def __next__(self):
try:
item = self.sequence[self.index]
except IndexError:
raise StopIteration()
self.index += 1
return item
def __iter__(self):
return self
if __name__ == '__main__':
# Создаем итератор по списку
it = Iterator([1, 2, 3, 4, 5, 6])
# Получаем элементы итератора и печатаем их
for i in it:
print(i)
# Возникнет исключение StopIteration, т.к. мы уже получили все элементы ранее
next(it)
1
2
3
4
5
6
Traceback (most recent call last):
...
IndexError: list index out of range
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
...
raise StopIteration()
Генераторная функция - это функция, которая содержит в своём теле ключевое слово yield и возвращает генератор.
Генератор в свою очередь является итератором, который порождает какие-либо значения и отдаёт их с использованием ключевого слова yield. yield приостанавливает поток выполнения программы и возвращает данные вызывающей стороне. В свою очередь вызывающая сторона также может что-то отправить в генератор и продолжить его выполнение. Таким образом мы подошли к такому важному и ключевому понятию как yield, которое позволяет управлять потоком выполнения программы. Встречая yield, функция замораживается и “уступает” своё выполнение другим функциям, возвращая управление вызывающей стороне. Последующие вызовы next() “пробуждают” функцию, которая работает до следующего yield либо return или завершения её тела. Пример генераторной функции:
def generator(sequence):
""" Генераторная функция """
index = 0
while 1:
try:
# Отправляем элемент последовательности вызывающей стороне и приостанавливаем поток выполнения программы
# Функции g.send(x) или next(g) дают сигнал продолжать выполнение, первая позволяет отправить сообщение
# в генератор.
value = yield sequence[index]
# Печатаем значение, оправленное в генератор
print(value)
except IndexError:
# Элементы закончились, вызывающая сторона получит исключение StopIteration
break
index += 1
if __name__ == '__main__':
# Создаем генератор
g = generator([1, 2, 3, 4, 5, 6])
# Инициализируем и получаем его первый элемент
item = next(g)
print(item)
x = 100
while 1:
try:
# Получаем следующие элементы и отсылаем в генератор значения
# Можно использовать next(g), если значения отсылать не требуется
item = g.send(x)
except StopIteration:
# Элементы закончились
break
print(item)
x += 100
1
100
2
200
3
300
4
400
5
500
6
600
Генератор, внутри которого происходит управление потоком выполнения программы можно считать сопрограммой. До PEP 492 генераторы и сопрограммы были идентичны с точки зрения семантики языка, однако в сопрограммах обычно происходит обмен данными с вызывающей стороной. Сопрограммы не генерируют данные, они используются для реализации невытесняющей многозадачности, где функции добровольно возвращают управление планировщику заданий и ожидают своей дальнейшей очереди. Python имеет следующие инструменты для этого:
next() - продолжить выполнение сопрограммы
send() - продолжить выполнение сопрограммы, отправив туда данные
throw() - возбудить ошибку в том месте, где сопрограмма остановилась
close() - возбудить ошибку GeneratorExit в том месте, где сопрограмма остановилась, т.е. завершить её
Следующий этап эволюции сопрограмм - это добавление синтаксиса yield from, который помогает разбивать генератор на подгенераторы. yield from делегирует управление от вызывающей стороне к другим генераторам, без написания стереотипного кода итерирования с обработкой ошибок.
Псевдокод yield from, поможет лучше понять, что делает конструкция у себя внутри:
import sys
def sub_generator():
""" Генератор, который порождает список чисел от 1 до 6 """
for i in [1, 2, 3, 4, 5, 6]:
x_from_main = yield i
print(f'sub_generator: полученное из main значение "{x_from_main}".')
def delegate_generator():
""" Классический генератор, который делегирует поток выполнения при помощи yield from """
yield from sub_generator()
def delegate_generator_2():
""" Функция, которая возвращает генераторную функцию, реализующую псевдокод yield from """
return yield_from(sub_generator())
def yield_from(expr):
"""
Генераторная функция, реализующая псевдокод yield from
https://www.python.org/dev/peps/pep-0380/#formal-semantics
:param expr: Итерируемый объект
:return:
"""
_i = iter(expr)
try:
# Инициализируем и получаем первое значение из итератора, в нашем случае 1
_y = next(_i)
except StopIteration as _e:
# В случае, если бы функция sub_generator имела бы вид
# def sub_generator():
# return 1
# произошло бы исключение StopIteration и _r стало бы равным 1
_r = _e.value
else:
while 1:
try:
# Отдаем полученное значение "1" вызывающей стороне и ждем следующей итерации
_s = yield _y
except GeneratorExit as _e:
try:
_m = _i.close
except AttributeError:
pass
else:
_m()
raise _e
except BaseException as _e:
_x = sys.exc_info()
try:
_m = _i.throw
except AttributeError:
raise _e
else:
try:
_y = _m(*_x)
except StopIteration as _e:
_r = _e.value
break
else:
try:
# Вызывающая сторона продолжила итерирование
if _s is None:
# Снова запрашиваем значение из итератора: 2, 3, 4 и т.д. пока не возникнет StopIteration
_y = next(_i)
else:
# Если вызывающая сторона отправила данные, то мы пересылаем их итератору
_y = _i.send(_s)
except StopIteration as _e:
# В итераторе закончились данные, выходим из цикла while
_r = _e.value
break
return _r
if __name__ == '__main__':
g = delegate_generator_2()
value_sub_generator = next(g)
print(f'main: полученное из sub_generator значение "{value_sub_generator}".')
x = 100
while 1:
try:
value_sub_generator = g.send(x * value_sub_generator)
print(f'main: полученное из sub_generator значение "{value_sub_generator}".')
except StopIteration:
break
main: полученное из sub_generator значение "1".
sub_generator: полученное из main значение "100".
main: полученное из sub_generator значение "2".
sub_generator: полученное из main значение "200".
main: полученное из sub_generator значение "3".
sub_generator: полученное из main значение "300".
main: полученное из sub_generator значение "4".
sub_generator: полученное из main значение "400".
main: полученное из sub_generator значение "5".
sub_generator: полученное из main значение "500".
main: полученное из sub_generator значение "6".
sub_generator: полученное из main значение "600".
PEP 343 дал старт использованию сопрограмм на основе генераторов и синтаксиса yield или yield from, который был улучшен в PEP 380. Однако данный подход имеет ряд недостатков:
С приходом PEP 492 и синтаксиса async/await все координально поменялось. Сопрограммы навсегда перестали ассоциироваться с генераторами. Они объявляются при помощи ключевого async, а конструкция yield заменена на await. Также сопрограмма не реализует __iter__ и __next__ и не может быть использована в качестве итератора. Однако внутренняя реализация сопрограмм основана на генераторах, поэтому они унаследовали такие методы как throw(), send() и close (), а исключения StopIteration и GeneratorExit играют такую же роль как для генераторов.
Ниже приведён список PEP, связанных с тем, как сопрограммы и невытесняющая многозадачность появлялись в Python.
18 мая 2001- Simple Generators - https://www.python.org/dev/peps/pep-0255/
Появилось понятие генератора и ключевого слова yield
10 мая 2005 - Coroutines via Enhanced Generators - https://www.python.org/dev/peps/pep-0342/
Появились сопрограммы как вариант улучшенных генераторов, которые обзавелись новыми функциями send, throw, close для взаимодействия с вызывающей стороной
13 февраля 2009 - Syntax for Delegating to a Subgenerator - https://www.python.org/dev/peps/pep-0380/
Введена синтаксическая конструкция yield from, позволяющая разбивать генераторы на подпрограммы без написания однотипного кода.
9 апреля 2015 - https://www.python.org/dev/peps/pep-0492/
Появились сопрограммы, определяемые ключевыми словами async/await, которыми мы привыкли сейчас пользоваться.
28 июля 2016 - Asynchronous Generators - https://www.python.org/dev/peps/pep-0525/
Появились асинхронные генераторы.