4
votes

I'm learning Python await / async syntax and wondering how coroutine can be implemented without async, await or yield. For example, I made this simple three seconds timer with async def syntax:

import asyncio

async def coroutine():
    count = 0
    while count < 3:
        count += 1
        print(count)
        await asyncio.sleep(1)

loop = asyncio.get_event_loop()
loop.run_until_complete(coroutine())
loop.close()

The result:

1
2
3

I noticed that we can implement a Coroutine object by implementing __await__ (https://docs.python.org/3.6/reference/datamodel.html#awaitable-objects). So I can successfully remove await.

import asyncio

class Generator():
    def __await__(self):
        count = 0
        while count < 3:
            count += 1
            print(count)
            yield from asyncio.sleep(1)

loop = asyncio.get_event_loop()
loop.run_until_complete(Generator())
loop.close()

Finally, I want to implement the iterator without yield like that:

import asyncio

class Iterator():

    def __init__(self):
        self.count = 0

    def __iter__(self): return self

    def __await__(self): return self

    def __next__(self):
        if self.count < 3:
            self.count += 1
            print(self.count)
            return next(asyncio.sleep(1))
        else:
            raise StopIteration()

loop = asyncio.get_event_loop()
result = loop.run_until_complete(Iterator())
loop.close()

But it didn't work. It stops after it shows '1'.

I know this doesn't have any practical value, but I want to know it to understand asyncio correctly. Can I implement a Coroutine without await or yield? If so how to do it? I tested it by Python 3.6.7.

1
With regards to yield, I think it's required, as it's what allows the interpreter to do a context switch.martineau
Probably yes, but I swill would like to know what is exactly missing in a hand written iterator compare to yield as a coroutine. If I return None in __next__() instead of asyncio.sleep(1), the loop doesn't complain it and works as I expect.Takashi Yamamiya

1 Answers

2
votes

While coroutines are implemented using generators, coroutines are not generators:

import asyncio
from typing import Generator


def gen():
    yield 1


async def coro():
    pass


print(isinstance(gen(), Generator))   # True
print(isinstance(coro(), Generator))  # False

And it is intentional since generator is nothing more than a detail of implementation towards coroutine. Same true for iterators: coroutines are not iterators.

loop.run_until_complete expects to recieve something awaitable like asyncio coroutine or future. You're trying to pass iterator - a different object with a different behavior.


Long story short, if you want to implement coroutine compatible with asyncio:

  • define functions with async def
  • or implement __await__ magic method

And read asyncio doc for examples.

If you want to understand general idea behind how coroutines work on very low-level you can follow this excellent video where David Beazley implements coroutine and event loop from scratch.