12

Consider the following program (running on 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())

I have 2 (related) questions:

  1. Even with the decorator on fast_sqrt, Python seems to optimize away the Future created in fast_sqrt altogether, and a plain float is returned. Which then blows up in run_test() in the yield from

  2. Why do I need to evaluate future.result() in run_test to retrieve the value of fire the exception? The docs say that yield from <future> "suspends the coroutine until the future is done, then returns the future’s result, or raises an exception". Why do I manually need to retieve the future's result?

Here is what I get:

oberstet@COREI7 ~/scm/tavendo/infrequent/scratchbox/python/asyncio (master)
$ python3 -V
Python 3.4.0b1
oberstet@COREI7 ~/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
oberstet@COREI7 ~/scm/tavendo/infrequent/scratchbox/python/asyncio (master)

Ok, I found the "issue". The yield from asyncio.sleep in slow_sqrt will make it a coroutine automatically. The waiting needs to be done differently:

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

All 4 variants are here.

oberstet
  • 21,353
  • 10
  • 64
  • 97
  • Does this work inPython 3.3 or is this just a bug in the beta? – mmmmmm Dec 22 '13 at 11:55
  • I don't have 3.3 installed .. will check. – oberstet Dec 22 '13 at 11:56
  • Regarding the first issue, *where* does it blow up? On `future = yield from f(x)` or on `res = future.result()`? –  Dec 22 '13 at 12:00
  • @delnan on `yield from f(x)` .. `float is not iterable`. In fact, without the decorator, `fast_sqrt` will return a `float` (not a `future` anymore). – oberstet Dec 22 '13 at 12:02
  • @oberstet I find that *extremely* hard to believe (it invalidates my understanding of `asyncio`, which otherwise explains what you describe). Can you double-check that the code posted here is the code causing that issue? In other words, please copy and paste the code from this question, remove the `@coroutine`, run it, and show the traceback. I'd do that myself but I have neither the 3.4 beta available nor a package manager to test in asyncio-for-3.3. –  Dec 22 '13 at 12:07
  • @delnan Added traceback and corrected question 1): in fact, it blows up with and without the decorator. The function `fast_sqrt` simply does not return a `future`, but a `float`. How can this be? Am I stupid? https://github.com/oberstet/scratchbox/blob/master/python/asyncio/test3.py – oberstet Dec 22 '13 at 12:20

1 Answers1

6

Regarding #1: Python does no such thing. Note that the fast_sqrt function you've written (i.e. before any decorators) is not a generator function, coroutine function, task, or whatever you want to call it. It's an ordinary function running synchronously and returning what you write after the return statement. Depending on the presence of @coroutine, very different things happen. It's just bad luck that both result the same error.

  1. Without the decorator, fast_sqrt(x) runs like the ordinary function it is and returns a future of a float (regardless of context). That future is consumed by the future = yield from ..., leaving future a float (which doesn't have a result method).

  2. With the decorator, the call f(x) goes through a wrapper function created by @coroutine. This wrapper function calls fast_sqrt and unpacks the resulting future for you, using the yield from <future> construction. Therefore, this wrapper function is itself a coroutine. Therefore, future = yield from ... waits on that coroutine and leaves future a float, again.

Regarding #2, yield from <future> does work (as explained above, you're using it when using the undecorated fast_sqrt), and you could also write:

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

(Modulo that it doesn't work for fast_sqrt as written, and gains you no extra async-ness because the future is already done by the time it's returned from coro_returning_a_future.)

Your core problem seems to be that you confuse coroutines and futures. Both your sqrt implementations try to be async tasks resulting in futures. From my limited experience, that's not how one usually writes asyncio code. It allows you to pull both the construction of the future and the computation which the future stands for into two independent async tasks. But you don't do that (you return an already-finished future). And most of the time, this is not a useful concept: If you have to do some computation asynchronously, you either write it as a coroutine (which can be suspended) or you push it into another thread and communicate with it using yield from <future>. Not both.

To make the square root computation async, just write a regular coroutine doing the computation and return the result (the coroutine decorator will turn fast_sqrt into a task that runs asynchronously and can be waited on).

@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)
  • I know how to do with coroutines and without Futures (https://github.com/oberstet/scratchbox/blob/master/python/asyncio/test2.py), but this _requires_ functions to be decorated. I am looking for a solution with futures and without decorators (non-invasive rgd API). – oberstet Dec 22 '13 at 12:52
  • @oberstet You can write an ordinary non-coroutine function that returns a future. You can write an ordinary function that returns ordinary values and use them synchronously. You can write a coroutine without the decorator (though you obviously still need to follow asyncio conventions regarding `yield from` etc.). I don't know what a "rgd API" is but if you want to desrcibe your problem in more detail, you can open another question. –  Dec 22 '13 at 12:57
  • My "issue" was that the `yield from asyncio.sleep` inside `slow_srqt` make it a coroutine automatically. I was looking for these 4 variants: https://github.com/oberstet/scratchbox/blob/master/python/asyncio/test3.py – oberstet Dec 22 '13 at 13:09