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_iterableest 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
awaitsur un itérateur (comme retourné paritertools.chain) n'est pas valide, carawaitné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 defdéclare une fonction comme une coroutine, permettant l'utilisation deawaità l'intérieur.awaitsuspend 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 deasyncio.sleepbloque toute la boucle d'événements. - Appeler des fonctions synchrones bloquantes (comme
requests.get) dans une coroutine sans utiliser des variantes asynchrones. - Oublier
awaitdevant 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.