Ok, so after looking into this a bit more, I'm not sure the docs are actually helpful past the basics. If you think about
why you're using async/await in the first place, it's so that you don't need to wait for external resources (a web request, db query, file reading) to finish before you continue working. It's so you can start something up, and while it's just waiting for a result, you can continue to work in the one thread (this isn't multithreading or multiprocessing, it's just using a single thread more efficiently), and then later, once you have to have that data to continue, you can a
wait for that data to finish being received.
These examples are about how to use the async/await syntax to use a library written that way, but they don't help you write the library. After reading the examples, I don't feel like you'd be in a better position than if you just wrote synchronous code and ignored async/await.
The fact that a coroutine doesn't actually do anything until you use await, also means that you can't just define an async function to work that way, either.
Let's take a hypothetical example of a database (DB). You query it, and it might take a few seconds for that query to run and results to get back to you. You should be able to use that time to do something else, instead of just waiting (or if you're a gui app, your whole app freezes while waiting for data?).
So, hypothetically, we would want to do this:
async def main():
await with DB.open() as connection:
query = connection.query("select * from spam")
# at this point, the query should be running on the remote server,
# and we should be able to do something else while it does it's thing
page_to_compare_results = requests.get("http://google.com")
# we can no longer do anything until we have the data from the db
# so NOW we await
data = await query
The problem, is that you simply can't write that using async/await. But the docs don't give an example of how you COULD do that. It is possible, though, and I'll show how later. First, I'd like to show why the async/await syntax can't do that.
>>> import asyncio
>>> async def foo():
... print("[foo] entered")
... print("[foo] waiting a bit")
... await asyncio.sleep(1)
... print("[foo] done waiting")
...
>>> async def main():
... print("[main] entered")
... print("[main] calling foo()")
... task = foo()
... print("[main] foo called")
... print("[main] awaiting foo")
... await task
... print("[main] await complete")
...
>>> asyncio.run(main())
[main] entered
[main] calling foo()
[main] foo called
[main] awaiting foo
[foo] entered
[foo] waiting a bit
[foo] done waiting
[main] await complete
Calling an async function does stark nothing at all (it returns a coroutine object) until you actually await it. Which means there's no setup, it doesn't start getting data, it just does nothing at all until you start waiting for it. Which means, writing code like this is not productive, and is worse than just not using async/await, as it makes your code less clear while also not providing any performance benefit.
This confused me at first, because in other languages (such as c#), calling an async function causes it to run immediately, and control flow doesn't return the the calling function until it uses await.
So if we wanted to write something that started running right away, and could later be awaited so the main thread can wait for data, we can do that using the special method
__await__
. But this special method cannot return a coroutine, AND it can't be async. Which means if you're chaining an async function, you need to return IT'S special __await__ coroutine. But it's possible, and works fine:
>>> import time
>>> class Spam:
... def __init__(self):
... print("[spam] called")
... self.start = time.time()
... def __await__(self):
... print("[spam] awaited")
... diff = time.time() - self.start
... sleep_for = 1 - diff
... print(f"[spam] will sleep for: {sleep_for}")
... return asyncio.sleep(sleep_for).__await__()
...
>>> async def main():
... print("[main] started")
... task = Spam()
... print("[main] after spam called")
... await asyncio.sleep(0.25)
... print("[main] after sleep")
... await task
... print("[main] after await")
...
>>> asyncio.run(main())
[main] started
[spam] called
[main] after spam called
[main] after sleep
[spam] awaited
[spam] will sleep for: 0.747551441192627
[main] after await
I just wish something like that, showing how you can actually make the bottom of the async pipeline (instead of just how to use something already in place), was somewhere in the docs.