[Async-sig] Optional async method and best practices

Cory Benfield cory at lukasa.co.uk
Wed Jul 12 04:18:25 EDT 2017


> On 11 Jul 2017, at 23:26, Laurent Mazuel via Async-sig <async-sig at python.org> wrote:
> 
> Hello,

Hi Laurent! A future note: your message got stuck in moderation because you aren’t subscribed to the mailing list. You may find it helpful to subscribe, as your future messages will also get stuck unless you do!

> My first prototype was to split and return a tuple (resource, coroutine):
> 
>    obj, optional_poller = client.create(**parameters)
>    obj = await optional_poller # OPTIONAL
> 
> But I got a warning if I decide to do not use this poller, RuntimeWarning: coroutine 'foo' was never awaited 
> 
> I was surprised honestly that I can't do that, since I feel like I'm not leaking anything. I didn't run the operation, so there is no wasted resource at my knowledge. But I remember wasting time because of a forgotten "yield from", so I guess it's fair 😊. But I would be curious to understand what I did badly.

The assumption in asyncio, generally speaking, is that you do not create coroutines you do not care about running. This is both for abstract theoretical reasons (if you don’t care if the coroutine is run or not, why not just optimise your code to never create the coroutine and save yourself the CPU cycles?) and for more concrete practical concerns (coroutines may own system resources and do cleanup in `finally` blocks, and if you don’t await a coroutine then you’ll never reach the `finally` block and so will leak system resources).

Given that there’s no computational way to be sure that *not* running a coroutine is safe (hello there halting problem), asyncio takes the pedantic model and says that not running a coroutine is a condition that justifies a warning. I think asyncio’s position here is correct, incidentally.

> 2- Return my initial object with an async method. This allows me to write (something finally close to the current code):
> 
>    async_poller = client.create(**parameters)
>    obj = async_poller.resource() # Get the initial resource information, but the object is not actually created yet. 
>    obj = await async_poller.result() # OPTIONAL
> 
> My async_poller object being something like:
> 
>    class PollerOperation:
>         async def result(self):
>              ...async version of previous sync result()...
> 
> So the questions are:
> - Does this seem a correct pattern?

Yes. This is the simple map to your old API, and is absolutely what I’d recommend doing in the first instance if you want to use coroutines.


> - Is there a simple way to achieve something like this:
> 
>    obj = await async_poller
> 
> meaning, I can win the "result()" syntax and directly "await" on the object and get the result from magic function. I tried by subclassing some ABC coroutine/awaitable, but wasn't able to find a correct syntax. I'm not even sure this makes sense and respects the zen of Python 😊

There are a few other patterns you could use.

The first is to return a Future, and just always run the “polling” function in the background to resolve that future. If the caller doesn’t care about the result they can just ignore the Future, and if they do care they can await on it. This has the downside of always requiring the I/O to poll, but is otherwise pretty clean.

Another option is to offer two functions, for example `def resource_nowait()` and `async def resource`. The caller can decide which they want to call based on whether they care about finding out the result. This is the clearest approach that doesn’t trigger automatic extra work like the Future does, and it lacks magic: it’s very declarative. This is a nice approach for keeping things clean.

Finally, you can create a magic awaitable object. PEP 492 defines several ways to create an “awaitable”, but one approach is to use what it calls a “Future-like object”: that is, one with a __await__ method that returns an iterator. In this case, you’d do a very basic extension of the Future object by triggering the work upon the call of __await__ before delegating to the normal behaviour. This is an annoyingly precise thing to do, though technically do-able.

Cory


More information about the Async-sig mailing list