Pourquoi la programmation asynchrone change tout

Tu dĂ©veloppes en Python et tu te demandes pourquoi certaines applications mettent 30 secondes Ă  rĂ©pondre alors qu’elles ne font « presque rien » ? Le coupable est souvent l’exĂ©cution synchrone : ton programme attend bĂȘtement qu’une opĂ©ration lente (requĂȘte HTTP, lecture fichier, appel base de donnĂ©es) se termine avant de passer Ă  la suite.

La programmation asynchrone avec async/await rĂ©sout ce problĂšme : elle permet Ă  ton code de continuer Ă  s’exĂ©cuter pendant qu’il attend des opĂ©rations lentes. RĂ©sultat : des applications jusqu’Ă  10 fois plus rapides sans ajouter un seul thread.

Dans ce tutoriel, tu vas apprendre les bases de la programmation asynchrone en Python avec des exemples concrets et progressifs. Commençons !

Qu’est-ce que la programmation asynchrone ?

Imagine un chef cuisinier qui doit prĂ©parer un repas complet. En mode synchrone, il met l’eau Ă  bouillir, reste devant la casserole jusqu’Ă  Ă©bullition, puis prĂ©pare la sauce, attend qu’elle mijote, puis coupe les lĂ©gumes. Il passe 80 % de son temps Ă  attendre.

En mode asynchrone, il met l’eau Ă  bouillir et, pendant ce temps, coupe les lĂ©gumes. Quand l’eau bout, il laisse les pĂątes cuire et, pendant ce temps, prĂ©pare la sauce. Il fait progresser plusieurs tĂąches sans jamais attendre inactivement.

C’est exactement ce que fait async/await en Python : quand une opĂ©ration est en attente (I/O), le programme exĂ©cute autre chose au lieu de bloquer.

Les bases : async et await

Commençons par le squelette théorique avant de passer à des exemples pratiques.

Une fonction asynchrone se déclare avec async def au lieu de def :

import asyncio

async def dire_bonjour():
    print("Bonjour...")
    await asyncio.sleep(1)  # Simule une attente (I/O)
    print("...le monde !")

# Pour exécuter une fonction async :
asyncio.run(dire_bonjour())

Les mots-clés essentiels :

  • async def — dĂ©clare une fonction asynchrone (une « coroutine »)
  • await — marque un point d’attente : le programme peut suspendre cette tĂąche et en exĂ©cuter une autre
  • asyncio.run() — point d’entrĂ©e qui lance la boucle d’Ă©vĂ©nements

Premier exemple : synchrone vs asynchrone

Prenons deux fonctions qui « attendent » 2 secondes chacune (simulant une requĂȘte rĂ©seau) :

Version synchrone (lent)

import time

def operation_lente(n):
    print(f"  Opération {n} commence...")
    time.sleep(2)  # Bloque tout !
    print(f"  Opération {n} terminée.")
    return n * 10

def main_sync():
    debut = time.time()
    resultats = []
    for i in range(4):
        resultats.append(operation_lente(i))
    duree = time.time() - debut
    print(f"Synchrone : {duree:.1f} secondes")
    return resultats

main_sync()
# Sortie : Synchrone : 8.0 secondes

Version asynchrone (rapide)

import asyncio

async def operation_lente_async(n):
    print(f"  Opération {n} commence...")
    await asyncio.sleep(2)  # Laisse la main Ă  d'autres tĂąches
    print(f"  Opération {n} terminée.")
    return n * 10

async def main_async():
    debut = time.time()
    # Lance toutes les opérations en parallÚle
    resultats = await asyncio.gather(
        operation_lente_async(0),
        operation_lente_async(1),
        operation_lente_async(2),
        operation_lente_async(3),
    )
    duree = time.time() - debut
    print(f"Asynchrone : {duree:.1f} secondes")
    return resultats

asyncio.run(main_async())
# Sortie : Asynchrone : 2.0 secondes

Avec asyncio.gather(), les 4 opĂ©rations s’exĂ©cutent en parallĂšle et le temps total passe de 8 secondes Ă  seulement 2 secondes. Le gain est de 75 % !

Cas pratique : requĂȘter plusieurs API simultanĂ©ment

Voici un exemple concret qui utilise aiohttp pour appeler 5 API en mĂȘme temps :

import asyncio
import aiohttp
import time

async def recuperer_url(session, url, n):
    print(f"🌐 RequĂȘte {n} : {url}")
    async with session.get(url) as response:
        data = await response.text()
        taille = len(data)
        print(f"✅ RequĂȘte {n} terminĂ©e : {taille} caractĂšres")
        return {"url": url, "taille": taille}

async def main():
    urls = [
        "https://jsonplaceholder.typicode.com/posts",
        "https://jsonplaceholder.typicode.com/comments",
        "https://jsonplaceholder.typicode.com/albums",
        "https://jsonplaceholder.typicode.com/photos",
        "https://jsonplaceholder.typicode.com/todos",
    ]
    
    async with aiohttp.ClientSession() as session:
        taches = [recuperer_url(session, url, i) for i, url in enumerate(urls)]
        resultats = await asyncio.gather(*taches)
    
    total = sum(r["taille"] for r in resultats)
    print(f"\n📊 Total : {total} caractĂšres rĂ©cupĂ©rĂ©s depuis {len(urls)} APIs")
    return resultats

debut = time.time()
asyncio.run(main())
duree = time.time() - debut
print(f"⏱ Temps total : {duree:.2f} secondes")

Si chaque requĂȘte prenait 1 seconde en synchrone, le total serait de 5 secondes. En asynchrone, le temps total est celui de la plus lente des requĂȘtes (environ 1 seconde).

Gérer les erreurs avec try/await/except

La gestion d’erreurs en asynchrone utilise la mĂȘme syntaxe familiĂšre :

import asyncio

async def requete_risquee(n):
    if n == 3:
        raise ValueError(f"Erreur simulĂ©e sur la requĂȘte {n}")
    await asyncio.sleep(1)
    return f"Résultat {n}"

async def main():
    taches = [requete_risquee(i) for i in range(5)]
    resultats = []
    
    for coro in asyncio.as_completed(taches):
        try:
            resultat = await coro
            resultats.append(resultat)
        except ValueError as e:
            print(f"⚠ Exception capturĂ©e : {e}")
    
    print(f"Résultats obtenus : {resultats}")

asyncio.run(main())

Tu remarqueras qu’avec asyncio.as_completed(), tu rĂ©cupĂšres les rĂ©sultats au fur et Ă  mesure qu’ils arrivent (pas besoin d’attendre la fin de toutes les tĂąches).

Limiter le nombre de tĂąches concurrentes

Si tu lances 1000 requĂȘtes simultanĂ©es, tu vas saturer ta connexion rĂ©seau (et l’API que tu appelles risque de te blacklister). La solution : un Semaphore (ou async with + limite) :

import asyncio

# Limiteur : 3 requĂȘtes simultanĂ©es maximum
semaphore = asyncio.Semaphore(3)

async def requete_limitee(n):
    async with semaphore:  # Attend si 3 requĂȘtes sont dĂ©jĂ  en cours
        print(f"🚀 DĂ©marrage requĂȘte {n}")
        await asyncio.sleep(1)
        print(f"✅ Fin requĂȘte {n}")
        return n

async def main():
    # Lance 10 requĂȘtes, mais jamais plus de 3 Ă  la fois
    taches = [requete_limitee(i) for i in range(10)]
    resultats = await asyncio.gather(*taches)
    print(f"Tous les résultats : {resultats}")

asyncio.run(main())

Tu verras que les requĂȘtes dĂ©marrent par groupes de 3, chaque groupe attendant qu’une place se libĂšre. C’est indispensable dĂšs que tu travailles avec des API externes.

Les piÚges à éviter

La programmation asynchrone en Python a quelques subtilités :

  1. ❌ time.sleep() dans une fonction async — time.sleep() bloque tout le programme, mĂȘme dans une coroutine. Utilise toujours await asyncio.sleep().

  2. ❌ Oublier await — Si tu Ă©cris ma_fonction_async() sans await, tu obtiens un objet coroutine, pas le rĂ©sultat. C’est une erreur frĂ©quente.

  3. ❌ MĂ©langer code synchrone et asynchrone — Appeler asyncio.run() plusieurs fois dans le mĂȘme script peut causer des erreurs. Utilise une seule boucle d’Ă©vĂ©nements.

  4. ❌ OpĂ©rations CPU-intensive — L’asynchrone est fait pour les opĂ©rations I/O (rĂ©seau, fichiers, BDD). Pour du calcul pur, utilise multiprocessing ou concurrent.futures.

Quand utiliser async/await (et quand éviter)

✅ À faire avec async❌ Pas adaptĂ©
RequĂȘtes HTTP / API RESTCalculs lourds (traitement images, ML)
Lecture/écriture fichiers multiplesOpérations synchrones bloquantes (input utilisateur)
RequĂȘtes base de donnĂ©esCode sĂ©quentiel simple (pas d’attente)
Web scraping de plusieurs pagesBibliothĂšques non async (certaines vieilles libs)
Websockets / temps réelScripts courts à usage unique

Conclusion et prochaines étapes

La programmation asynchrone avec async/await est l’un des super-pouvoirs de Python moderne. Elle te permet d’Ă©crire des applications plus rapides, plus rĂ©actives et plus efficaces sans complexitĂ© supplĂ©mentaire — juste en comprenant quand et comment dĂ©lĂ©guer l’attente.

Commence par utiliser asyncio.gather() dans tes prochains projets qui font des appels rĂ©seau. C’est le plus simple Ă  intĂ©grer et le gain est immĂ©diat.

📘 MaĂźtrise les fondamentaux de Python — Si ce tutoriel t’a aidĂ©, le cours complet Python : Les Fondamentaux couvre tout ce qu’il faut pour devenir autonome en programmation Python : variables, fonctions, POO, modules et projets pratiques. DĂ©couvrir le cours →

Photo de Julia M Cameron sur Pexels