Quietly and quietly (c),
Python version 3.5 came out ! And, of course, one of the most interesting innovations of the release is the new syntax of coroutines using the
async / await keywords, later in the article about it.
A superficial review of
"PEP 0492 - Coroutines with async and await syntax" at first left me with the question "Why is this necessary." Coroutines are satisfactorily implemented on extended generators, and at first glance it may seem that it all came down to replacing
yield from with
await , and the decorator creating the coroutine with
async . You can add the feeling that everything is done solely for use with the
asyncio module.
But this, of course, is not so, the subject is deeper and more interesting.coroutine
The main thing, probably, is that the coroutine in Python is now a special object of
native coroutine , and not in some special way designed generator or something else. This object has methods and functions of the standard library for working with it. That is, now it is an object defined as part of the language.
')
await
Unfortunately, I didn’t find a short definition in the documentation and PEP for which this new keyword was entered. I would venture to formulate it myself:
The await keyword indicates that when the expression following it is executed, it is possible to switch from the current coroutine to another or to the main execution thread.Accordingly, the expression after
await is also not simple, it should be an
awaitable object.
awaitable object
There are three options for awaitable objects:
- Another coroutine is the native coroutine object. This is reminiscent, and apparently implemented in the same way as when another generator is called in the generator using yield from .
- A coroutine based on a generator created using the types.coroutine () decorator. This is an option to ensure compatibility with practices, where coroutines are implemented on the basis of generators.
- A special object that implements the __await__ magic method, which returns an iterator. Using this iterator, the result of the coroutine execution is returned.
An example of how to write your own
awaitable object was not found in the PEP or in the documentation, at least at the time of this writing this is not the case. This flaw will be corrected below.)
async
The PEP has a dozen paragraphs with the headings "Why ..." and "Why not ...". Almost all of them are devoted to the question of why this keyword is used in this way, and not in any other way. Indeed,
async def looks weird in the code, and does it cause reflections on the subject of the “pythonic way”? On the other hand, it is clear that they wanted some more holistic picture, since there is also
async for and
async with .
- async def - defines the native coroutine function , the result of which is called, the native coroutine coroutine object, which is not yet running.
- async for - determines that the iterator used in the loop, when receiving the next value, can switch execution from the current coroutine. The object has an iterator instead of the standard magic methods: __iter__ and __next__ , methods: __aiter__ and __anext__ . Functionally, they are similar, but as follows from the definition, they allow the use of await in their bodies.
- async with - determines that when entering the context block and exiting it, there may be a switch of execution from the current coroutine. As in the case of an asynchronous generator, instead of the magic methods: __enter__ and __exit__ , you should use functionally similar __aenter__ and __aexit__ .
In the definitions given in the PEP, it is written that “asynchronous code” can be invoked in magic methods. It seems to me that “switching execution from the current coroutine” is a more correct option. The use of the term "asynchronous code" can be misleading, because "asynchronous code" is often implemented on callback functions, and this is a slightly different topic.
Examples of the use of asynchronous iterators and the context of managers in the documentation and PEP are sufficient, usecase is generally understandable and everything is logical. One thing is not clear - why use versions of magic methods with different names, because they are still declared using `async def`. Apparently, this is something related to the peculiarities of implementation, I do not see another explanation.
How to cook it?
The study of a new language feature or library quickly rests on the question of how and where to use it. And a deeper study, in my opinion, should be continued on a practical example. For me, if the topic is related to coroutines, asynchrony, and the like, such a practical example is the writing of a hellowword using an event-driven approach. The task is formulated as follows: “The call to the sleep function should stop the execution of the coroutine for a certain time.”
Coroutines and event-driven are perfectly combined, in
my other article in more detail, why I think so. And an example of this kind will show us well almost all the possibilities and nuances of using coroutines.
Suppose that we have an event dispatcher running in the main execution thread, which, when an expected event occurs, calls the callback function. Then the practical implementation may be:
- The sleep function sets up the event manager to call the callback function after a specified period of time. After that, it switches control to the main execution thread (that is, to the dispatcher).
- The callback function passed to the dispatcher is called after a specified time has passed. It switches to a coroutine with the transfer of some useful information to it.
It can be coded like this:
from time import time from collections import deque from tornado.ioloop import IOLoop current = deque() class sleep(object): def __init__(self, timeout): self.deadline = time() + timeout def __await__(self): def swith_to(coro): current.append(coro) coro.send(time()) IOLoop.instance().add_timeout(self.deadline, swith_to, current[0]) current.pop() return (yield) def coroutine_start(run, *args, **kwargs): coro = run(*args, **kwargs) current.append(coro) coro.send(None) if __name__ == '__main__': async def hello(name, timeout): while True: now = await sleep(timeout) print("Hello, {}!\tts: {}".format(name, now)) coroutine_start(hello, "Friends", 1.0) coroutine_start(hello, "World", 2.5) IOLoop.instance().start()
As you can see, the code is short, fairly understandable and, by the way, working. I will describe the main points in it:
- As the event dispatcher, tornado.ioloop.IOLoop is used to comment on my opinion there is nothing special.
- The sleep class implements an awaitable object, its function is to transfer control to the event dispatcher, having previously configured it to call callback after a specified period of time.
- The callback function is defined as a closure, but in this case it does not matter. Its purpose is to simply switch the execution back to a coroutine with the transfer of the current time. Switching the execution to a coroutine is done by calling its send method or throw method to switch with throwing an exception.
- The purpose of the coroutine_start function is to create a coroutine by calling the factory function and launching it for execution. The first call to the coroutine send method, must be with the parameter None - this starts the coroutine
- The hello function itself is trivial. Maybe that's understandable, but I think it is worth clarifying. This feature is not coroutine! This function, which creates and returns a coroutine (factory function), is similar to the functions that create and return a generator.
The development of this idea: “async / await coroutine and event-driven”, can be viewed
at this link . It is still raw, but in addition to the shown switching on the timeout event, the coroutines switching on the I / O ready and system sygnal events is implemented. As a demo, there is an example of an asynchronous echo server.
Finally
Coroutines in one form or another have been available for quite some time, but there were no “official rules of the game with them.” Now these “rules of the game” are defined and fixed, and this will be a good reason to use asynchronous programming methods more widely in Python.