282 lines
4.7 KiB
Markdown
282 lines
4.7 KiB
Markdown
# asyncio (ou pas)
|
||
|
||
## Julien Palard
|
||
|
||
- Software Engineer
|
||
- cPython core dev
|
||
- Coordinator of docs.python.org/fr/
|
||
- Python trainer
|
||
- hackinscience.org
|
||
|
||
|
||
# Coroutine
|
||
|
||
Une coroutine est une fonction dont l'exécution peut être suspendue.
|
||
|
||
|
||
## Callback Hell
|
||
|
||
```
|
||
function pong_handler(client)
|
||
{
|
||
client.on('data', function (data)
|
||
{
|
||
client.on('data_written', function ()
|
||
{
|
||
client.close()
|
||
});
|
||
client.write(data)
|
||
client.flush()
|
||
});
|
||
}
|
||
```
|
||
|
||
|
||
## Avec des coroutines
|
||
|
||
```python
|
||
async def pong_handler():
|
||
client.write(await client.read())
|
||
await client.flush()
|
||
client.close()
|
||
```
|
||
|
||
## Les coroutines
|
||
|
||
Pour être exhaustif, il existe deux types de coroutines en Python :
|
||
|
||
- generator-based coroutines
|
||
- native coroutines
|
||
|
||
|
||
## Generator-based coroutines
|
||
|
||
```pytho
|
||
import types
|
||
|
||
|
||
@types.coroutine
|
||
def get_then_print(url):
|
||
...
|
||
```
|
||
|
||
|
||
## Native coroutines
|
||
|
||
```python
|
||
async def get_then_print(url):
|
||
...
|
||
```
|
||
|
||
|
||
## Coroutines
|
||
|
||
Une `coroutine`, renvoie un objet `coroutine` :
|
||
|
||
```
|
||
>>> async def tum():
|
||
... print("tum")
|
||
...
|
||
>>> tum()
|
||
<coroutine object tum at 0x7fa294538468>
|
||
```
|
||
|
||
|
||
## Coroutines
|
||
|
||
Qu'on peut manipuler :
|
||
|
||
|
||
```
|
||
>>> async def tum():
|
||
... print("tum")
|
||
...
|
||
>>> a_coroutine_object = tum()
|
||
>>> a_coroutine_object.send(None)
|
||
tum
|
||
Traceback (most recent call last):
|
||
File "<stdin>", line 1, in <module>
|
||
StopIteration
|
||
```
|
||
|
||
Notes:
|
||
|
||
As you can see, calling `tum()` did not execute the `print("tum")`,
|
||
but calling `.send(None)` did (see PEP 342).
|
||
|
||
L'appel de .send est fait par la main loop (asyncio.run).
|
||
|
||
|
||
## Coroutines
|
||
|
||
Une coroutine renvoie juste un objet coroutine, donc à un moment, du code devra appeler la méthode `send` dessus.
|
||
|
||
C'est le rôle d'une *main loop*.
|
||
|
||
|
||
## Récupérer un résultat
|
||
|
||
Le résultat d'une coroutine est stocké dans l'exception `StopIteration`.
|
||
|
||
Notes:
|
||
|
||
Dans l'attribut `value`.
|
||
|
||
|
||
## await
|
||
|
||
Il existe un mot clé dédié à la récupération de résultat d'une coroutine :
|
||
|
||
`await`
|
||
|
||
|
||
## await
|
||
|
||
Lorsqu'on lui donne un *awaitable*, `await` essaye d'obtenir son résultat.
|
||
|
||
Si l'*awaitable* se suspend, `await` suspends à son tour la coroutine actuelle, récursivemet, jusqu'à l'appel à `send`.
|
||
|
||
|
||
## await
|
||
|
||
|
||
```
|
||
async def two():
|
||
return 2
|
||
|
||
async def four():
|
||
return await two() + await two()
|
||
|
||
coro = four()
|
||
coro.send(None)
|
||
```
|
||
|
||
Ce qui donne `StopIteration: 4`, de manière complètement synchrone, malgré le vocabulaire utilisé.
|
||
|
||
|
||
## Suspendre une coroutine.
|
||
|
||
Ce n'est pas possible dans une coroutine.
|
||
|
||
Mais toute la chaîne d'*await* peut-être suspendue en utilisant un
|
||
`yield` depuis un *future-like object*.
|
||
|
||
|
||
## Future-like object
|
||
|
||
Un `future-like object` est un object implémentant `__await__`, qui a
|
||
le droit de `yield`. L'expression du yield traversera toute la stack
|
||
d'`await` jusqu’au `send`.
|
||
|
||
|
||
## Awaitables
|
||
|
||
Les [awaitables](https://www.python.org/dev/peps/pep-0492/#await-expression)
|
||
sont des objets pouvant être « attendus » via un `await`.
|
||
|
||
Donc c'est soit une `coroutine` soit un objet implémentant `__await__`.
|
||
|
||
|
||
# Gérer ses coroutines
|
||
|
||
Appelons simplement `send`, et attendons-nous à la fameuse `StopIteration` :
|
||
|
||
```python
|
||
async def two():
|
||
return 2
|
||
|
||
async def four():
|
||
return await two() + await two()
|
||
|
||
def coro_manager(coro):
|
||
try:
|
||
coro.send(None)
|
||
except StopIteration as stop:
|
||
return stop.value
|
||
|
||
print(coro_manager(four()))
|
||
```
|
||
|
||
|
||
## Gérer ses coroutines
|
||
|
||
Mais si la coroutine se suspend, notre `coro_manager` ne suffira pas :
|
||
|
||
```python
|
||
class Awaitable:
|
||
def __await__(self):
|
||
yield
|
||
|
||
async def wont_terminate_here():
|
||
await Awaitable()
|
||
print("Terminated")
|
||
return 42
|
||
|
||
print(coro_manager(wont_terminate_here()))
|
||
```
|
||
|
||
## Gérer ses coroutines
|
||
|
||
Donc, un gestionnaire de coroutines doit rappeler send, comme celui-ci :
|
||
|
||
```python
|
||
def frenetic_coro_manager(coro):
|
||
try:
|
||
while True:
|
||
coro.send(None)
|
||
except StopIteration as stop:
|
||
return stop.value
|
||
```
|
||
|
||
## Gérer ses coroutines
|
||
|
||
Mais notre manager frénétique ne sait s'occuper que d'une seule
|
||
routine, peut-on faire mieux ?
|
||
|
||
|
||
## Gérer ses coroutines
|
||
|
||
On pourrait relancer le travail de l'une d'entre elles au hasard :
|
||
|
||
```python
|
||
import random
|
||
|
||
|
||
def frenetic_coros_manager(*coros):
|
||
coros = list(coros)
|
||
while coros:
|
||
coro = random.choice(coros)
|
||
try:
|
||
coro.send(None)
|
||
except StopIteration as stop:
|
||
coros.remove(coro)
|
||
```
|
||
|
||
|
||
## Gérer ses coroutines
|
||
|
||
Essayons :
|
||
|
||
```python
|
||
async def tum():
|
||
while True:
|
||
await Awaitable()
|
||
print("Tum")
|
||
|
||
async def pak():
|
||
while True:
|
||
await Awaitable()
|
||
print("Pak")
|
||
|
||
frenetic_coros_manager(tum(), pak())
|
||
```
|
||
|
||
Ohh, deux `while True:` qui coopèrent !
|
||
|
||
|
||
# Questions?
|
||
|
||
- mamot.fr/@mdk
|
||
- github.com/JulienPalard
|
||
- mdk on #python-fr on libera.chat
|