Python asyncio, фьючерсы и доходность

Рассмотрим следующую программу (запущенную на CPython 3.4.0b1):

import math
import asyncio
from asyncio import coroutine

@coroutine
def fast_sqrt(x):
   future = asyncio.Future()
   if x >= 0:
      future.set_result(math.sqrt(x))
   else:
      future.set_exception(Exception("negative number"))
   return future


def slow_sqrt(x):
   yield from asyncio.sleep(1)
   future = asyncio.Future()
   if x >= 0:
      future.set_result(math.sqrt(x))
   else:
      future.set_exception(Exception("negative number"))
   return future


@coroutine
def run_test():
   for x in [2, -2]:
      for f in [fast_sqrt, slow_sqrt]:
         try:
            future = yield from f(x)
            print("\n{} {}".format(future, type(future)))
            res = future.result()
            print("{} result: {}".format(f, res))
         except Exception as e:
            print("{} exception: {}".format(f, e))


loop = asyncio.get_event_loop()
loop.run_until_complete(run_test())

У меня есть 2 (связанных) вопроса:

  • Даже с декоратором на fast_sqrt, Python, похоже, полностью оптимизирует будущее, созданное в fast_sqrt, и возвращается обычный float. Которая затем взрывается в run_test() в yield from

  • Почему мне нужно оценить future.result() в run_test, чтобы получить значение огненного исключения? docs docs говорят, что yield from <future> "приостанавливает сопрограмму до тех пор, пока не будет сделано будущее, а затем вернет результат фьючерса или вызовет исключение". Почему мне нужно вручную удалить будущий результат?

Вот что я получаю:

[email protected] ~/scm/tavendo/infrequent/scratchbox/python/asyncio (master)
$ python3 -V
Python 3.4.0b1
[email protected] ~/scm/tavendo/infrequent/scratchbox/python/asyncio (master)
$ python3 test3.py

1.4142135623730951 <class 'float'>
<function fast_sqrt at 0x00B889C0> exception: 'float' object has no attribute 'result'

Future<result=1.4142135623730951> <class 'asyncio.futures.Future'>
<function slow_sqrt at 0x02AC8810> result: 1.4142135623730951
<function fast_sqrt at 0x00B889C0> exception: negative number

Future<exception=Exception('negative number',)> <class 'asyncio.futures.Future'>
<function slow_sqrt at 0x02AC8810> exception: negative number
[email protected] ~/scm/tavendo/infrequent/scratchbox/python/asyncio (master)

Хорошо, я нашел "проблему". yield from asyncio.sleep в slow_sqrt сделает это сопрограммой автоматически. Ожидание должно выполняться по-другому:

def slow_sqrt(x):
   loop = asyncio.get_event_loop()
   future = asyncio.Future()
   def doit():
      if x >= 0:
         future.set_result(math.sqrt(x))
      else:
         future.set_exception(Exception("negative number"))
   loop.call_later(1, doit)
   return future

Все 4 варианта здесь.

Ответ 1

Относительно # 1: Python не делает этого. Обратите внимание, что функция fast_sqrt, которую вы написали (то есть перед любыми декораторами), не является функцией генератора, сопрограммой, заданием или тем, что вы хотите назвать. Это обычная функция, выполняемая синхронно и возвращающая то, что вы пишете после инструкции return. В зависимости от наличия @coroutine происходят самые разные вещи. Это просто невезение, что оба они приводят к той же ошибке.

  • Без декоратор fast_sqrt(x) работает как обычная функция и возвращает будущее float (независимо от контекста). Это будущее потребляется future = yield from ..., оставляя future поплавок (который не имеет метода result).

  • С декоратором вызов f(x) проходит через функцию-обертку, созданную @coroutine. Эта функция обертки вызывает fast_sqrt и распаковывает полученное будущее для вас, используя конструкцию yield from <future>. Следовательно, эта функция-оболочка сама является сопрограммой. Поэтому future = yield from ... ждет этого сопрограммы и снова оставляет future поплавок.

Что касается №2, yield from <future> работает (как объяснялось выше, вы используете его при использовании unecorated fast_sqrt), и вы также можете написать:

future = yield from coro_returning_a_future(x)
res = yield from future

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

Ваша основная проблема заключается в том, что вы путаете сопрограммы и фьючерсы. Обе реализации sqrt пытаются выполнить асинхронные задачи, приводящие к фьючерсам. Из моего ограниченного опыта это не то, как обычно пишется асинхронный код. Это позволяет вам тянуть как строительство будущего, так и вычисления, которые будущее означает для двух независимых задач async. Но вы этого не делаете (вы возвращаете уже готовое будущее). И большую часть времени это не очень полезная концепция: если вам нужно выполнить какое-то вычисление асинхронно, вы либо записываете его как сопрограмму (которая может быть приостановлена), либо вы вставляете ее в другой поток и обмениваетесь ею с помощью yield from <future>, Не оба.

Чтобы сделать вычисление квадратного корня асинхронным, просто напишите регулярную сопрограмму, выполняющую вычисления, и результат return (декоратор coroutine превратит fast_sqrt в задачу, выполняющуюся асинхронно и ее можно будет ждать).

@coroutine
def fast_sqrt(x):
   if x >= 0:
      return math.sqrt(x)
   else:
      raise Exception("negative number")

@coroutine # for documentation, not strictly necessary
def slow_sqrt(x):
   yield from asyncio.sleep(1)
   if x >= 0:
      return math.sqrt(x)
   else:
      raise Exception("negative number")

...
res = yield from f(x)
assert isinstance(res, float)