Question 1Étant donné async def hello(): return 'Hi', que retourne hello() ?
Bases d'async / await — Accélérer un programme en réutilisant le temps d'attente
Coroutines, event loop et asyncio.sleep, puis comment async def/await réutilise le temps d'attente d'I/O en lançant d'autres tâches pour accélérer ton code.
async / await est un mécanisme pour faire d'autres tâches pendant que tu attends une I/O (Input/Output — lectures de fichiers, appels réseau, requêtes BD et autres opérations dominées par le temps d'attente). Il permet d'accélérer des tâches comme appeler 100 fois une Web API, sans jamais quitter un seul thread. Cet article couvre trois idées clés : les coroutines, l'event loop et asyncio.sleep.
À propos de l'exécution du code ici
async / await tourne autour du timing, mais le runner de ce site bufferise la sortie de print et l'affiche d'un coup une fois le script terminé. La sortie en temps réel et le ressenti du temps écoulé ne correspondront pas à un vrai environnement Python. L'article utilise des schémas pour rendre le comportement interne clair, mais si tu veux voir print couler en direct ou ressentir le vrai timing, exécute le code dans ton environnement Python local avec asyncio.run.
Processus et thread — le contexte d'async / await
Avant de plonger dans async / await, posons clairement processus et thread.
Un processus (un programme en cours d'exécution du point de vue de l'OS) est une unité d'exécution indépendante avec son propre espace mémoire. Lance un script Python et tu obtiens un processus ; lances-en un autre dans un autre terminal et tu en as deux.
Un thread (le flux qui fait réellement tourner le code à l'intérieur d'un processus) est l'unité qui utilise le CPU pour avancer dans le code. Un processus peut avoir plusieurs threads, mais un programme Python typique tourne en 1 processus + 1 thread, et une attente comme time.sleep arrête ce thread.
- Une unité d'exécution indépendante du point de vue de l'OS
- Possède son propre espace mémoire
- L'unité qui fait tourner le code à l'intérieur d'un processus
- Un Python ordinaire en a typiquement un seul qui tourne
- Échange les tours à l'intérieur d'un seul thread
- Tu peux en créer autant que tu veux avec
async / await
threading (qui ajoute des threads) et multiprocessing (qui ajoute des processus).async / await ne gère que les coroutines au niveau le plus interne. Il bascule entre coroutines sur le même thread, sans jamais ajouter de thread ni de CPU — il passe simplement la main à une autre coroutine pendant que le CPU est inactif à attendre une I/O. Pour faire tourner des threads vraiment en parallèle, utilise threading ; pour ajouter des processus, utilise multiprocessing (couverts dans les deux articles suivants).
Qu'est-ce qu'une coroutine — la star d'async / await
Une coroutine (une fonction / un objet défini avec async def qui peut se mettre en pause à des points await internes) est au cœur d'async / await. Une fonction ordinaire s'exécute d'un trait jusqu'à la fin quand on l'appelle, mais une coroutine peut se mettre en pause à chaque await interne et reprendre quand l'event loop le lui dit. C'est la fondation sur laquelle async est bâti.
Appeler une fonction async def n'exécute pas son corps — cela retourne juste un objet coroutine (par exemple, coro = hello()). Le corps ne commence à s'exécuter que quand tu fais await coro ou que tu le passes à asyncio.run(coro).
import asyncio
async def hello():
return "Hi"
# L'appeler n'exécute pas le corps
coro = hello()
print(coro) # <coroutine object hello at 0x...>
# ↑ 0x... est une adresse mémoire.
# Cela veut juste dire "une coroutine a été créée"
# await l'exécute réellement
print(await coro) # Hi
return la termine. Les await internes mettent en pause et reprennent entre les deux.Quand tu écris await some_io(), la coroutine se met en pause et bascule vers une autre tâche, et d'autres coroutines peuvent tourner pendant qu'elle attend — c'est la caractéristique qui définit async.
Qu'est-ce que l'event loop — l'ordonnanceur qui bascule entre coroutines
L'event loop (un ordonnanceur qui bascule entre coroutines à tour de rôle) est ce que asyncio exécute en coulisses. Il boucle sans fin : prendre une coroutine dans la file → l'exécuter → basculer vers une autre à await → remettre les attentes terminées dans la file.
import asyncio
async def main():
print("start")
await asyncio.sleep(0) # sleep(0) n'attend pas vraiment,
# il marque juste "OK pour basculer ici"
print("end")
# asyncio.run démarre la loop → exécute main → ferme la loop à la fin
asyncio.run(main())
# Sortie :
# start
# end
Tout cela se passe sur un seul thread — pas de cœurs CPU supplémentaires, juste remplir le temps d'attente inactif avec une autre coroutine. Dans le Python du navigateur de ce site, l'event loop tourne déjà, donc tu peux écrire await directement au top level sans appeler asyncio.run(...).
Pourquoi async / await — basculer vers une autre tâche pendant les attentes
Dans du code synchrone (sync) (le style ligne par ligne habituel), time.sleep(1) bloque le programme — le CPU est inactif, mais le programme est figé. Idem pour les réponses Web API, les requêtes BD et la fin des I/O fichier.
async / await permet de basculer vers une autre tâche partout où tu écris « attendre », en restant sur le même thread mais en basculant à l'instant où une attente s'enclenche.
# requests = client HTTP sync (un appel à la fois)
# httpx = client HTTP async-capable (await en parallèle)
import requests, asyncio, httpx
# Sync : appeler 3 APIs une par une → 3 secondes au total
def fetch_users_sync():
r1 = requests.get("https://api.example.com/users/1") # ← attend 1 sec
r2 = requests.get("https://api.example.com/users/2") # ← encore 1 sec
r3 = requests.get("https://api.example.com/users/3") # ← encore 1 sec
return [r1.json(), r2.json(), r3.json()]
# Async : les 3 en parallèle → finit en ~1 sec (le plus lent)
async def fetch_users_async():
async with httpx.AsyncClient() as client:
r1, r2, r3 = await asyncio.gather(
client.get("https://api.example.com/users/1"),
client.get("https://api.example.com/users/2"),
client.get("https://api.example.com/users/3"),
)
return [r1.json(), r2.json(), r3.json()]
Détails d'asyncio.gather à venir
asyncio.gather(...) utilisé ici exécute plusieurs coroutines en concurrence et attend tous les résultats. La sémantique détaillée — valeurs de retour, gestion des exceptions — est couverte dans l'article suivant : asyncio Tasks.
Concurrent, pas parallèle
async / await n'ajoute jamais de CPU — du code qui sature le CPU (calcul intensif) ne sera pas accéléré. Il ne fait que remplir le temps CPU inactif pendant « attente I/O / attente réseau / sleep » en basculant vers une autre tâche. C'est de l'exécution concurrente, pas de la vraie exécution parallèle. Pour vraiment tourner sur plusieurs cœurs, il faut threading ou multiprocessing — couverts dans les deux articles suivants.
async def et await — les bases
Une fonction définie avec async def est appelée une fonction coroutine — l'appeler n'exécute pas le corps, ça retourne juste un objet coroutine. Pour vraiment l'exécuter, fais await dessus ou passe-la à asyncio.run().
await x signifie « attendre que x se termine, en basculant vers une autre tâche entre-temps ». x peut être une de trois choses : une coroutine (sur quoi cet article se concentre), une Task, ou un Future (l'objet Task couvert dans l'article suivant, plus l'objet de notification de complétion qu'il utilise en interne) — au quotidien, tu utiliseras surtout des coroutines ou des Tasks.
import asyncio
# Définir une fonction coroutine avec async def
async def hello():
return "Hi"
# L'appeler retourne juste un objet coroutine (le corps ne tourne pas)
print(hello()) # <coroutine object hello at 0x...>
# await l'exécute (le top-level await fonctionne dans cet environnement navigateur)
result = await hello()
print(result) # Hi
# Dans un vrai script Python, encadre avec asyncio.run()
# print(asyncio.run(hello())) # Hi
| Élément | Signification | Notes |
|---|---|---|
| async def f(): | Définit une fonction coroutine | L'appeler n'exécute pas le corps |
| f() | Crée un objet coroutine | Nécessite await pour s'exécuter |
| await f() | Attend la fin en basculant vers d'autres | Légal seulement dans une fonction async |
| asyncio.sleep(N) | Attend N secondes (en cédant la main) | Ne bloque pas comme time.sleep |
| asyncio.run(f()) | Exécute depuis le top level | Point d'entrée standard en vrai Python |
Sans await, la coroutine ne tourne jamais
Si tu écris hello() tout seul, le corps ne tourne jamais, et tu verras un avertissement comme <coroutine object hello at 0x...> dans la console. Toujours soit await hello() soit l'exécuter via asyncio.run(hello()). « Appeler » et « exécuter » sont deux choses différentes en async — garde ça en tête.
L'exécution concurrente partage l'attente — le bénéfice d'asyncio
asyncio.sleep(seconds) est la version async de sleep — il bascule vers une autre tâche pendant qu'il attend. Combiné à asyncio.gather (article suivant) pour exécuter plusieurs coroutines à la fois, tous les sleeps progressent en concurrence, donc le temps total devient le sleep le plus long (pas la somme, comme te le donnerait time.sleep).
En interne, asyncio.sleep(N) enregistre « reprends cette coroutine dans N secondes » auprès de la loop et bascule immédiatement. À l'inverse, time.sleep(N) est un appel OS qui bloque complètement le CPU — la loop s'arrête aussi, et aucune autre coroutine ne peut tourner. Utiliser time.sleep à l'intérieur d'une fonction async ruine tout l'intérêt d'async, donc fais attention.
Vérification des connaissances
Répondez à chaque question une par une.
Question 2Dans une fonction async, lequel « attend N secondes en basculant vers une autre tâche » ?
Question 3Quelle charge de travail tire le plus profit d'async / await ?