Aplatir les résultats de coroutines en une séquence en Python

Lors du travail avec des fonctions asynchrones en Python, il est courant de devoir combiner les listes retournées par plusieurs appels de coroutines en une seule séquence. Cet article explique les erreurs typiques et les approches correctes pour collecter et aplatir les résultats.

Concaténation incorrecte des résultats de coroutines

Considérons un scénario où une fonction asynchrone traiter_lot retourne une liste d'éléments, et nous voulons exécuter cette fonction sur plusieurs lots et combiner les résultats. Une tentative incorrecte serait :

donnees = await itertools.chain.from_iterable(traiter_lot(client, texte, lot) for lot in lots_de_noeuds)

Cela échoue pour deux raisons :

  • itertools.chain.from_iterable est une fonction synchrone qui ne gère pas les coroutines ; elle traite les objets coroutine comme des objets ordinaires, sans les exécuter.
  • L'utilisation de await sur un itérateur (comme retourné par itertools.chain) n'est pas valide, car await nécessite un objet awaitable comme une coroutine ou une tâche.

Approche correcte avec asyncio.gather

Pour obtenir tous les résultats d'abord, puis les aplatir, nous devons d'abord exécuter toutes les coroutines de manière concurrente et collecter leurs résultats. Ensuite, nous pouvons combiner les listes.

import asyncio
import itertools

async def traiter_lot(client, texte, lot):
    # Simulation d'un traitement asynchrone retournant une liste
    await asyncio.sleep(0.1)  # Simule une opération d'E/S
    return [f"{texte}_{i}" for i in lot]

async def main():
    lots_de_noeuds = [[1, 2], [3, 4], [5, 6]]
    # Étape 1 : Exécution concurrente de toutes les coroutines
    resultats_partiels = await asyncio.gather(
        *(traiter_lot("client1", "donnee", lot) for lot in lots_de_noeuds)
    )
    # Étape 2 : Aplatissement des listes en une séquence unique
    sequences_aplaties = list(itertools.chain.from_iterable(resultats_partiels))
    print(sequences_aplaties)  # Output: ['donnee_1', 'donnee_2', 'donnee_3', 'donnee_4', 'donnee_5', 'donnee_6']

# Alternative plus concise avec une compréhension de liste
async def main_alternative():
    lots_de_noeuds = [[1, 2], [3, 4], [5, 6]]
    sequences_aplaties = [
        element
        for sous_liste in await asyncio.gather(
            *(traiter_lot("client1", "donnee", lot) for lot in lots_de_noeuds)
        )
        for element in sous_liste
    ]
    print(sequences_aplaties)

asyncio.run(main())

Cette méthode garantit que toutes les coroutines sont exécutées en parallèle via asyncio.gather, et les résultats sont ensuite combinés efficacement.

Introduction à async et await

Les mots-clés async et await forment la base de la programmation asynchrone en Python. Voici un aperçu :

  • async def déclare une fonction comme une coroutine, permettant l'utilisation de await à l'intérieur.
  • await suspend l'exécution de la coroutine jusqu'à ce que l'objet awaitable (comme une coroutine ou une tâche) soit terminé, sans bloquer le boucle d'événements.

Exemple de base

import asyncio

async def afficher_apres(delai, message):
    await asyncio.sleep(delai)
    print(message)

async def executer():
    # Exécution séquentielle
    await afficher_apres(1, "Bonjour")
    await afficher_apres(2, "Monde")

asyncio.run(executer())

Ici, le temps total d'exécution est d'environ 3 secondes, car les appels sont séquentiels.

Exécution concurrente avec create_task

async def executer_concurrent():
    tache1 = asyncio.create_task(afficher_apres(1, "Premier"))
    tache2 = asyncio.create_task(afficher_apres(2, "Deuxième"))
    await tache1
    await tache2

asyncio.run(executer_concurrent())

Le temps total est réduit à environ 2 secondes, car les coroutines s'exécutent en parallèle.

Collecte de résultats avec gather

async def recuperer_donnees(url):
    await asyncio.sleep(0.5)  # Simule une requête HTTP
    return f"Contenu de {url}"

async def collecter_plusieurs():
    urls = ["http://example.com/1", "http://example.com/2", "http://example.com/3"]
    resultats = await asyncio.gather(*(recuperer_donnees(u) for u in urls))
    print(resultats)  # Output: ['Contenu de http://example.com/1', ...]

asyncio.run(collecter_plusieurs())

asyncio.gather permet d'exécuter plusieurs coroutines en parallèle et de collecter tous les résultats dans une liste.

Erreurs fréquentes

  • Utiliser time.sleep à la place de asyncio.sleep bloque toute la boucle d'événements.
  • Appeler des fonctions synchrones bloquantes (comme requests.get) dans une coroutine sans utiliser des variantes asynchrones.
  • Oublier await devant une coroutine, ce qui retourne l'objet coroutine au lieu de son résultat.
  • Appliquer await à des objets non awaitables, comme des itérateurs ou des fonctions ordinaires.

Pour gérer la concurrence avec des limites, on peut utiliser asyncio.Semaphore ou asyncio.as_completed pour un contrôle plus fin.

Étiquettes: Python asyncio coroutine async-await programmation asynchrone

Publié le 4 juillet à 07h33