Leçon 9 / 20

Identifier les opérations bloquantes

20 min Python — typage, tests et performance Gratuit

Objectif : Distinguer les opérations d’entrée-sortie du calcul qui peut rester synchrone.

Lorsqu’un outil de dépenses consulte un service externe, une grande partie du temps peut être passée à attendre une réponse plutôt qu’à calculer. Si cette attente bloque le programme, les autres travaux prévus ne peuvent pas avancer pendant ce temps. Dans cette leçon, vous allez repérer cette différence et écrire une consultation asynchrone sans modifier le résultat métier.

Reconnaître l’attente dans un traitement

Une entrée-sortie, souvent abrégée en E/S, échange des données avec une ressource extérieure au calcul courant : un service distant, un fichier ou une base de données. Pendant cet échange, le programme attend que la ressource réponde. Une opération est dite bloquante lorsque cette attente immobilise le fil d’exécution courant : les instructions suivantes ne sont pas exécutées avant la fin de l’opération.

À l’inverse, calculer un total à partir de dépenses déjà chargées est un travail effectué localement par le processeur. Ce calcul peut rester synchrone : vous appelez une fonction et récupérez son résultat immédiatement, selon l’ordre normal du programme. Gardez cette séparation dans l’outil de dépenses : la récupération des données relève des E/S, tandis que leur validation ou leur agrégation relève du calcul métier.

Écrire une coroutine pour une consultation

Le modèle d’exécution asyncio permet de suspendre temporairement une opération d’entrée-sortie sans bloquer tout le mécanisme qui gère les opérations asynchrones. Une fonction déclarée avec async def est une coroutine. À l’intérieur, await indique un point où la coroutine attend le résultat d’une opération asynchrone ; pendant cette attente, le contrôle peut revenir à la boucle d’événements.

Exemple autonome de consultation asynchroneTexte
import asyncio


async def consulter_depenses() -> list[dict[str, str | float]]:
    """Simule l'attente d'une réponse contenant des dépenses."""
    print("Consultation démarrée")
    await asyncio.sleep(0.2)
    print("Réponse reçue")
    return [
        {"description": "Repas", "montant": 18.5},
        {"description": "Transport", "montant": 4.2},
    ]


def calculer_total(depenses: list[dict[str, str | float]]) -> float:
    """Additionne les montants déjà reçus."""
    return sum(
        montant
        for depense in depenses
        if isinstance((montant := depense["montant"]), float)
        or isinstance(montant, int)
    )


async def main() -> None:
    depenses = await consulter_depenses()
    total = calculer_total(depenses)
    print(f"Total : {total:.2f} €")


if __name__ == "__main__":
    asyncio.run(main())
Résultat attendu
Consultation démarrée
Réponse reçue
Total : 22.70 €

L’exemple s’exécute directement avec Python et ne contacte aucun service réel : asyncio.sleep simule une attente asynchrone. La fonction consulter_depenses est une coroutine parce qu’elle est déclarée avec async def ; elle cède le contrôle au point await, puis retourne les données simulées. La fonction calculer_total, elle, n’attend aucune ressource : elle reste synchrone.

La coroutine main attend le résultat de la consultation avant de calculer le total. asyncio.run sert ici de point d’entrée : il démarre la boucle d’événements, exécute la coroutine principale jusqu’à son terme, puis termine proprement. Si vous lancez ce code, le résultat métier reste déterministe : deux montants, 18,50 € et 4,20 €, donnent un total de 22,70 €.

Dans le projet, cette distinction prépare le rôle de src/depenses/collecte.py : ce module sera consacré à la collecte asynchrone, tandis que le calcul des agrégations reste dans le service. L’exemple de cette leçon est autonome afin de ne pas supposer la forme exacte des modèles ou des fonctions déjà définis dans les autres fichiers. La consultation simulée ne doit donc pas être confondue avec une modification à appliquer telle quelle au code du projet.

À retenir

  • Une E/S attend une ressource extérieure ; si elle est bloquante, le fil d’exécution courant est immobilisé pendant cette attente.
  • Un calcul sur des données déjà disponibles peut rester une fonction synchrone.
  • Une coroutine se déclare avec async def ; await marque l’attente d’une opération asynchrone.
  • asyncio.run exécute une coroutine principale dans la boucle d’événements.
  • Une opération bloquante ne devient pas non bloquante simplement parce qu’elle est appelée depuis une coroutine.

Dans la prochaine leçon, vous pourrez faire avancer plusieurs consultations asynchrones sans attendre que chacune se termine séparément.

Envie d'aller plus loin ?

Les cours Enovatic sont gratuits et le resteront. Si vous cherchez un accompagnement encadré, nos formations professionnelles sont animées par les mêmes formateurs.