MB-F
MB-F

Reputation: 23637

Suspend coroutine and return to caller

Short Story:

yield cannot be used in an async def function to suspend execution and return to the caller. Is there a different way to do this in Python 3.5?

Edit: Answers to a very similar question suggest either to use Python 3.6 or to define an asynchronous generator as a class. Unfortunatley, the former is not an option at the moment. The generator could work but defining a class for this purpose seems to be more clunky than my own attempt.

Presumably, there is no clean and easy solution?

Long Story:

I need to write functions that can suspend their operation, return to their caller, and continue later where they stopped. These could be implemented as state machines, possibly using classes, which tears apart relevant code and is generally rather unreadable. With generators, Python offers a more or less elegant way to write such functions:

def a():
    print('A Part 1')
    yield  # suspend execution to caller
    print('A Part 2')

def run(func):
    o = func()
    try:
        while True:
            print('running...')
            o.send(None)
    except StopIteration:
        pass    

run(a)
# running...
# A Part 1
# running...
# A Part 2

Python 3.3 added the yield from syntax, which allows nice nesting of such functions. Instead of returning execution to the caller, yield from transfers execution to another function:

def b():
    print('B Part 1')
    yield from a()
    print('B Part 2')

run(b)
# running...
# B Part 1
# A Part 1
# running...
# A Part 2
# B Part 2

Python 3.5 introduced async def and await to distinguish coroutines from generators. Of course I would rather use these native coroutines. It is easy to rewrite b, simply by replacing def with async def and yield from with await. However, I did not find a canonical way to suspend a coroutine and return to the caller. yield is not allowed in an async function, and await only takes another function to run. I came up with this awkward solution:

import asyncio

@asyncio.coroutine
def awkward_suspend():
    yield

async def c():
    print('C Part 1')
    #yield  # SyntaxError: 'yield' inside async function 
    #await  # Syntax Error: invalid syntax
    await awkward_suspend()
    print('C Part 2')    

run(c)
# running...
# C Part 1
# running...
# C Part 2

This approach wraps yield in a normal function, thus creating a generator, and marks the generator as a coroutine so it can be awaited. This feels very much like an abuse of the language. Is there a way to achieve the same result without asyncio and the awkward suspend function?

Upvotes: 3

Views: 943

Answers (1)

panda-34
panda-34

Reputation: 4209

You can instead simply use sleep(0):

async def c():
    print('C Part 1')
    await asyncio.sleep(0)
    print('C Part 2')

sleep(0) does exactly the awkward_suspend - a simple yield In fact they have a private __sleep0() function there with that yield statement.

Upvotes: 2

Related Questions