Cours Python · Module 6: Async et FastAPI
L'asynchronicité - des observations en parallèle
Dans cette leçon12
Bienvenue dans le Module 6 ! Ici Darwin avec un sujet passionnant - la programmation asynchrone en Python !
Jusqu'à présent, tous tes programmes étaient synchrones - ils exécutaient les opérations les unes après les autres. Maintenant tu vas découvrir l'asynchronicité - une technique qui te permet d'accomplir plusieurs tâches en même temps !
Analogie safari : La programmation synchrone, c'est comme un seul guide qui observe les animaux un par un - d'abord les lions (il attend 30 minutes), puis les éléphants (il attend 20 minutes), puis les guépards (il attend 15 minutes). L'asynchrone, c'est comme plusieurs guides qui observent en même temps - l'un aux lions, un autre aux éléphants, un troisième aux guépards - tous en parallèle ! ⏱
Le problème du code synchrone
Code synchrone - attendre en séquence
1import time
2
3def observe_lions():
4 """Observation des lions - prend 3 secondes"""
5 print("Début de l'observation des lions...")
6 time.sleep(3) # Simulation de l'attente
7 print("Observation des lions terminée !")
8 return {"species": "Lion", "count": 5}
9
10def observe_elephants():
11 """Observation des éléphants - prend 2 secondes"""
12 print("Début de l'observation des éléphants...")
13 time.sleep(2)
14 print("Observation des éléphants terminée !")
15 return {"species": "Elephant", "count": 3}
16
17def observe_cheetahs():
18 """Observation des guépards - prend 4 secondes"""
19 print("Début de l'observation des guépards...")
20 time.sleep(4)
21 print("Observation des guépards terminée !")
22 return {"species": "Cheetah", "count": 2}
23
24# Exécution synchrone - UNE PAR UNE
25start = time.time()
26result1 = observe_lions() # Attend 3s
27result2 = observe_elephants() # Puis attend 2s
28result3 = observe_cheetahs() # Puis attend 4s
29end = time.time()
30
31print(f"\nTemps total : {end - start:.1f}s")
32# Sortie : Temps total : 9.0s (3 + 2 + 4)Problème : Temps = 9 secondes ! Nous devons attendre chaque observation en séquence, alors que nous pourrions observer en parallèle ! ⏰
La solution - le code asynchrone
Code asynchrone - attendre en parallèle
1import asyncio
2
3async def observe_lions():
4 """Observation asynchrone des lions"""
5 print("Début de l'observation des lions...")
6 await asyncio.sleep(3) # await au lieu de time.sleep !
7 print("Observation des lions terminée !")
8 return {"species": "Lion", "count": 5}
9
10async def observe_elephants():
11 """Observation asynchrone des éléphants"""
12 print("Début de l'observation des éléphants...")
13 await asyncio.sleep(2)
14 print("Observation des éléphants terminée !")
15 return {"species": "Elephant", "count": 3}
16
17async def observe_cheetahs():
18 """Observation asynchrone des guépards"""
19 print("Début de l'observation des guépards...")
20 await asyncio.sleep(4)
21 print("Observation des guépards terminée !")
22 return {"species": "Cheetah", "count": 2}
23
24async def main():
25 """Fonction async principale"""
26 start = time.time()
27
28 # Exécution en parallèle !
29 results = await asyncio.gather(
30 observe_lions(),
31 observe_elephants(),
32 observe_cheetahs()
33 )
34
35 end = time.time()
36 print(f"\nTemps total : {end - start:.1f}s")
37 # Sortie : Temps total : 4.0s (le max de 3, 2, 4)
38 print(f"Résultats : {results}")
39
40# Lance le main async
41asyncio.run(main())Résultat : Temps = 4 secondes ! Toutes les observations s'exécutent en parallèle !
Économie : 9s → 4s = 5 secondes de gagnées (55 % de réduction) !
Async/await - la syntaxe
async def - définir une fonction asynchrone
1# Fonction synchrone
2def sync_function():
3 return "Résultat sync"
4
5# Fonction asynchrone (coroutine)
6async def async_function():
7 return "Résultat async"async def crée une coroutine - une fonction spéciale qui peut être « suspendue » (await) puis reprise plus tard.
await - attendre un résultat
1async def fetch_species_data(species_id):
2 print(f"Récupération des données de l'espèce {species_id}...")
3 await asyncio.sleep(2) # Simulation d'une opération d'E/S
4 return {"id": species_id, "name": "Lion", "population": 120}
5
6async def main():
7 # await suspend l'exécution jusqu'à réception d'un résultat
8 data = await fetch_species_data(1)
9 print(f"Données reçues : {data}")
10
11asyncio.run(main())await dit : « Attends le résultat, mais laisse les autres tâches s'exécuter entre-temps ».
IMPORTANT : await ne peut être utilisé que à l'intérieur d'un async def !
Event Loop - le moteur async
L'event loop (boucle d'événements) est le cœur de l'asynchronicité - elle gère toutes les coroutines et bascule de l'une à l'autre.
Analogie : L'event loop est comme un coordinateur de safari - il attribue les guides aux différentes tâches, bascule entre elles et collecte les résultats !
1import asyncio
2
3async def task1():
4 print("Début tâche 1")
5 await asyncio.sleep(1)
6 print("Fin tâche 1")
7
8async def task2():
9 print("Début tâche 2")
10 await asyncio.sleep(0.5)
11 print("Fin tâche 2")
12
13async def main():
14 # L'event loop gère ces tâches
15 await asyncio.gather(task1(), task2())
16
17# asyncio.run() crée l'event loop et exécute main()
18asyncio.run(main())Sortie :
1Début tâche 1
2Début tâche 2
3Fin tâche 2 (après 0.5s)
4Fin tâche 1 (après 1s)L'event loop basculait entre task1 et task2 !
asyncio.gather() - exécution en parallèle
asyncio.gather() exécute plusieurs coroutines en parallèle et renvoie une liste de résultats :
1import asyncio
2
3async def get_species(species_id):
4 await asyncio.sleep(1)
5 return {"id": species_id, "name": f"Species {species_id}"}
6
7async def main():
8 # Récupère 5 espèces en parallèle
9 results = await asyncio.gather(
10 get_species(1),
11 get_species(2),
12 get_species(3),
13 get_species(4),
14 get_species(5)
15 )
16
17 print(f"{len(results)} espèces récupérées :")
18 for species in results:
19 print(f" - {species['name']}")
20
21asyncio.run(main())Temps : 1 seconde (au lieu de 5 secondes en synchrone) !
gather() renvoie les résultats dans le même ordre que celui dans lequel tu as fourni les coroutines !
asyncio.create_task() - lancer en arrière-plan
create_task() démarre une coroutine en arrière-plan (sans attendre son résultat) :
1async def background_observation(species):
2 print(f"Début de l'observation de {species}...")
3 await asyncio.sleep(3)
4 print(f"Observation de {species} terminée !")
5
6async def main():
7 # Lance en arrière-plan
8 task1 = asyncio.create_task(background_observation("Lion"))
9 task2 = asyncio.create_task(background_observation("Elephant"))
10
11 print("On fait autre chose entre-temps...")
12 await asyncio.sleep(1)
13 print("On continue à faire autre chose...")
14
15 # Attend la fin des tâches
16 await task1
17 await task2
18
19asyncio.run(main())Sortie :
1Début de l'observation de Lion...
2Début de l'observation de Elephant...
3On fait autre chose entre-temps...
4On continue à faire autre chose...
5Observation de Lion terminée !
6Observation de Elephant terminée !Les async comprehensions
Python autorise les compréhensions de liste/dictionnaire asynchrones :
1async def get_population(species_id):
2 await asyncio.sleep(0.1)
3 return species_id * 10
4
5async def main():
6 # Compréhension de liste asynchrone
7 populations = [await get_population(i) for i in range(1, 6)]
8 print(f"Populations : {populations}")
9 # Sortie : Populations : [10, 20, 30, 40, 50]
10
11 # Mais ceci s'exécute SÉQUENTIELLEMENT !
12 # Mieux vaut utiliser gather() :
13 populations = await asyncio.gather(
14 *[get_population(i) for i in range(1, 6)]
15 )
16 print(f"Populations (en parallèle) : {populations}")
17
18asyncio.run(main())Exemple pratique - un client API Safari
Un client d'API Safari asynchrone :
1import asyncio
2import aiohttp # pip install aiohttp
3
4class SafariAPIClient:
5 def __init__(self, base_url: str):
6 self.base_url = base_url
7
8 async def get_species(self, species_id: int):
9 """Récupère les données d'une espèce"""
10 async with aiohttp.ClientSession() as session:
11 async with session.get(f"{self.base_url}/species/{species_id}") as response:
12 return await response.json()
13
14 async def get_multiple_species(self, species_ids: list[int]):
15 """Récupère plusieurs espèces en parallèle"""
16 async with aiohttp.ClientSession() as session:
17 tasks = []
18 for species_id in species_ids:
19 task = session.get(f"{self.base_url}/species/{species_id}")
20 tasks.append(task)
21
22 responses = await asyncio.gather(*tasks)
23 results = []
24 for response in responses:
25 data = await response.json()
26 results.append(data)
27
28 return results
29
30async def main():
31 client = SafariAPIClient("https://api.safari-db.com")
32
33 # Récupère 10 espèces en parallèle
34 species_ids = list(range(1, 11))
35 start = time.time()
36 results = await client.get_multiple_species(species_ids)
37 end = time.time()
38
39 print(f"{len(results)} espèces récupérées en {end - start:.2f}s")
40
41asyncio.run(main())En synchrone : 10 requêtes × 0,5s = 5 secondes En asynchrone : max(0,5s) = 0,5 seconde
Quand utiliser l'async ?
Utilise l'async quand :
- Opérations d'E/S - requêtes HTTP, bases de données, fichiers
- Beaucoup d'opérations en parallèle - plus de 100 requêtes API
- WebSockets - communication en temps réel
- Attente - sleep, minuteurs, opérations longues
N'utilise PAS l'async quand :
- CPU-bound - calculs mathématiques, traitement d'images
- Code simple - petite application sans E/S
- Pas de bibliothèques async - la bibliothèque ne prend pas en charge l'async
Pour le CPU-bound, utilise le multiprocessing plutôt que l'async !
Les async context managers
1class AsyncDatabaseConnection:
2 async def __aenter__(self):
3 print("Connexion à la base de données...")
4 await asyncio.sleep(1)
5 return self
6
7 async def __aexit__(self, exc_type, exc_val, exc_tb):
8 print("Fermeture de la connexion...")
9 await asyncio.sleep(0.5)
10
11 async def query(self, sql):
12 print(f"Exécution : {sql}")
13 await asyncio.sleep(0.2)
14 return [{"id": 1, "name": "Lion"}]
15
16async def main():
17 async with AsyncDatabaseConnection() as db:
18 results = await db.query("SELECT * FROM species")
19 print(f"Résultats : {results}")
20
21asyncio.run(main())La gestion des erreurs en async
1async def risky_operation(species_id):
2 if species_id == 3:
3 raise ValueError(f"L'espèce {species_id} n'existe pas !")
4 await asyncio.sleep(1)
5 return {"id": species_id}
6
7async def main():
8 try:
9 results = await asyncio.gather(
10 risky_operation(1),
11 risky_operation(2),
12 risky_operation(3), # Erreur !
13 return_exceptions=True # Renvoie les exceptions au lieu de les lever
14 )
15
16 for i, result in enumerate(results, 1):
17 if isinstance(result, Exception):
18 print(f"Espèce {i} : ERREUR - {result}")
19 else:
20 print(f"Espèce {i} : OK - {result}")
21
22 except Exception as e:
23 print(f"Erreur : {e}")
24
25asyncio.run(main())Sortie :
1Espèce 1 : OK - {'id': 1}
2Espèce 2 : OK - {'id': 2}
3Espèce 3 : ERREUR - L'espèce 3 n'existe pas !Récapitulatif
Dans cette leçon, tu as appris :
- Sync vs Async - différences et avantages
async defetawait- la syntaxe- Event loop - le moteur de l'asynchronicité
asyncio.gather()- l'exécution en parallèleasyncio.create_task()- les tâches en arrière-plan- Les async comprehensions
- Des exemples pratiques d'API Safari
- Quand utiliser l'async (E/S) vs le multiprocessing (CPU)
- Les async context managers et la gestion des erreurs
Analogie safari finale : L'async, c'est comme plusieurs guides de safari qui observent différentes espèces en même temps - au lieu d'attendre 9 secondes en observant une par une (sync), ils observent en parallèle et terminent en 4 secondes (async) ! L'event loop, c'est le coordinateur qui orchestre tous les guides ! ⏱
Prochaine leçon : Darwin va te montrer FastAPI - un framework async moderne pour construire des API ultra-rapides !
Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Qu'est-ce que la programmation asynchrone ?
2. Comment définit-on une fonction asynchrone en Python ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri vertical
Ordonne une fonction asynchrone simple en Python :
- Clique dans l'ordre
Ordonne l'appel asyncio.gather pour lancer plusieurs coroutines :
- Éditeur de code
Écris une fonction async observe_animal qui attend 1 seconde (asyncio.sleep) et renvoie le nom de l'animal.