Of course it is possible to start an async
function without explicitly using asyncio
. After all, asyncio
is written in Python, so all it does, you can do too (though sometimes you might need other modules like selectors
or threading
if you intend to concurrently wait for external events, or paralelly execute some other code).
In this case, since your function has no await
points inside, it just needs a single push to get going. You push a coroutine by send
ing None
into it.
>>> foo().send(None)
Hello!
Hello!
...
Of course, if your function (coroutine) had yield
expressions inside, it would suspend execution at each yield
point, and you would need to push additional values into it (by coro.send(value)
or next(gen)
) - but you already know that if you know how generators work.
import types
@types.coroutine
def bar():
to_print = yield 'What should I print?'
print('Result is', to_print)
to_return = yield 'And what should I return?'
return to_return
>>> b = bar()
>>> next(b)
'What should I print?'
>>> b.send('Whatever you want')
Result is Whatever you want
'And what should I return?'
>>> b.send(85)
Traceback...
StopIteration: 85
Now, if your function had await
expressions inside, it would suspend at evaluating each of them.
async def baz():
first_bar, second_bar = bar(), bar()
print('Sum of two bars is', await first_bar + await second_bar)
return 'nothing important'
>>> t = baz()
>>> t.send(None)
'What should I print?'
>>> t.send('something')
Result is something
'And what should I return?'
>>> t.send(35)
'What should I print?'
>>> t.send('something else')
Result is something else
'And what should I return?'
>>> t.send(21)
Sum of two bars is 56
Traceback...
StopIteration: nothing important
Now, all these .send
s are starting to get tedious. It would be nice to have them semiautomatically generated.
import random, string
def run_until_complete(t):
prompt = t.send(None)
try:
while True:
if prompt == 'What should I print?':
prompt = t.send(random.choice(string.ascii_uppercase))
elif prompt == 'And what should I return?':
prompt = t.send(random.randint(10, 50))
else:
raise ValueError(prompt)
except StopIteration as exc:
print(t.__name__, 'returned', exc.value)
t.close()
>>> run_until_complete(baz())
Result is B
Result is M
Sum of two bars is 56
baz returned nothing important
Congratulations, you just wrote your first event loop! (Didn't expect it to happen, did you?;) Of course, it is horribly primitive: it only knows how to handle two types of prompts, it doesn't enable t
to spawn additional coroutines that run concurrently with it, and it fakes events by a random
generator.
(In fact, if you want to get philosophical: what we did above that manually, could also be called an event loop: Python REPL was printing prompts to a console window, and it was relying on you to provide events by typing t.send(whatever)
into it.:)
asyncio
is just an immensely generalized variant of the above: prompts are replaced by Future
s, multiple coroutines are kept in queues so each of them eventually gets its turn, and events are much richer and include network/socket communication, filesystem reads/writes, signal handling, thread/process side-execution, and so on. But the basic idea is still the same: you grab some coroutines, juggle them in the air routing the Futures from one to another, until they all raise StopIteration
. When all coroutines have nothing to do, you go to external world and grab some additional events for them to chew on, and continue.
I hope it's all much clearer now. :-)