talks/2018-paris.py-14-asyncio.md

4.7 KiB
Raw Permalink Blame History

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

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

import types


@types.coroutine
def get_then_print(url):
    ...

Native coroutines

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 jusquau send.

Awaitables

Les awaitables 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 :

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 :

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 :

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 :

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 :

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