Эволюция от итератора к сопрограммам

Для того, что бы понять, как работают сопрограммы и невытесняющая многозадачность в 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 expr

Следующий этап эволюции сопрограмм - это добавление синтаксиса yield from, который помогает разбивать генератор на подгенераторы. 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".
    

Acync/await

PEP 343 дал старт использованию сопрограмм на основе генераторов и синтаксиса yield или yield from, который был улучшен в PEP 380. Однако данный подход имеет ряд недостатков:

  1. Синтаксически, генераторы и сопрограммы идентичны с той лишь разницей, что генераторы порождают данные, а сопрограммы служат для выполнения нескольких задач под управлением одного планировщика.
  2. Наличие ключевых слов yield или yield from говорит разработчику о том, что функция является сопрограммой. Это совсем не очевидно в случае большого тела функции и может привести к случайным ошибкам при рефакторинге.
  3. Поддержка асинхронных вызовов ограничена выражениями, где допустим yield.

С приходом 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/
Появились асинхронные генераторы.