2
votes

I have been trying to understand asynchronous programming, particularly in Python. I understand that asyncio is built off of an event loop which schedules the execution of coroutines, but I have read about several different ways to define coroutines, and I am confused how they all relate to each other.

I read this article for more background information on the topic. Although it covers each of the four types of coroutines I have mentioned, it does not entirely describe how they differ. Without any external modules, a coroutine can be created using yield as an expression on the right side of an equals, and then data can be inputted through the .send(). However, code examples using the @asyncio.coroutine and @types.coroutine decorators do not ever use .send() from what I've found. Code examples from the article are below:

# Coroutine using yield as an expression
def coro():
    hello = yield "Hello"
    yield hello
c = coro()
print(next(c), end=" ")
print(c.send("World")) # Outputs Hello World

# Asyncio generator-based coroutine
@asyncio.coroutine
def display_date(num, loop):
    end_time = loop.time() + 50.0
    while True:
        print("Loop: {} Time: {}".format(num, datetime.datetime.now()))
        if (loop.time() + 1.0) >= end_time:
            break
        yield from asyncio.sleep(random.randint(0, 5))

# Types generator-based coroutine
@types.coroutine
def my_sleep_func():
    yield from asyncio.sleep(random.randint(0, 5))

# Native coroutine in Python 3.5+
async def display_date(num, loop, ):
    end_time = loop.time() + 50.0
    while True:
        print("Loop: {} Time: {}".format(num, datetime.datetime.now()))
        if (loop.time() + 1.0) >= end_time:
            break
        await asyncio.sleep(random.randint(0, 5))

My questions are:

  1. How do the yield coroutines relate to the types or asyncio decorated coroutines, and where is the .send() functionality utilized?
  2. What functionality do the decorators add to the undecorated generator-based coroutine?
  3. How do the @asyncio.coroutine and @types.coroutine decorators differ? I read this answer to try and understand this, but the only difference mentioned here is that the types coroutine executes like a subroutine if it has no yield statement. Is there anything more to it?
  4. How do these generator-based coroutines differ in functionality and in implementation from the latest native async/await coroutines?
1
yield is intended to send values out to what ever is using your generator. What gets sent back to your generator is totally dependent on what code is handling your generator. In the case of coroutines your generator is handled by an event loop of some kind (it calls the .send for you) and you get some guarantee that the value passed into the generator is directly produced from the value you passed out, hence why “await” is a far more intuitive word for coroutines. - Tadhg McDonald-Jensen
@TadhgMcDonald-Jensen How do the asyncio.coroutine and types.coroutine decorators differ in purpose and functionality? - Rohan
I don't actually know which is why I didn't post an answer, will take a look at the source code and see if I can find a desirable answer for you. - Tadhg McDonald-Jensen
See an important difference between @asyncio.coroutine and @types.coroutine here: stackoverflow.com/a/49477233/2085626 - Amir Kirsh

1 Answers

1
votes

you will likely laugh, I took a look at the source code for asyncio.coroutine and found it uses types.coroutine (any comment with #!> is added by me)

def coroutine(func):
    """Decorator to mark coroutines...."""
 #!> so clearly the async def is preferred.
    warnings.warn('"@coroutine" decorator is deprecated since Python 3.8, use "async def" instead',
                  DeprecationWarning,
                  stacklevel=2)
    if inspect.iscoroutinefunction(func):
 #!> since 3.5 clearly this is returning something functionally identical to async def.
        # In Python 3.5 that's all we need to do for coroutines
        # defined with "async def".
        return func 

    if inspect.isgeneratorfunction(func):
        coro = func
    else:
        #!> omitted, makes a wrapper around a non generator function.
#!> USES types.coroutine !!!!
    coro = types.coroutine(coro)
    if not _DEBUG:
        wrapper = coro
    else:
        #!> omitted, another wrapper for better error logging.

    wrapper._is_coroutine = _is_coroutine  # For iscoroutinefunction().
    return wrapper

So I think this comes down to historical stuff only, asyncio existed for longer than types so the original handling was done here, then when types came along the true wrapper was moved there and asyncio continued to just have some extra wrapping stuff. But at the end of the day, both are just to mimic the behaviour of async def