Files
ENI-PythonAdvanced_03/README.md
2025-12-15 15:58:02 +01:00

154 lines
7.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TP : Téléchargements synchrone et asynchrone (asyncio)
## Informations générales
**Cours** : Python Avancé > Programmation concurrente > Tâches IO-bound concurrentes avec asyncio \
**Objectifs pédagogiques** :
- Python avancé : programmation asynchrone
- Outils modernes (poetry, PyCharm)
- Bonnes pratiques de l'entreprise
---
## Prérequis
### Installation et configuration de lenvironnement
- Faites `poetry install` après import du projet (fichier lock déja présent), pour s'assurer que toutes les dépendances sont installées.
- Assurez-vous que l'interpréteur Python du projet pointe bien vers l'environnement virtuel créé par Poetry, ce qui devrait être le cas par défaut (sinon cliquer en bas à droite sur le nom de l'interpréteur puis `Add new interpreter`, ensuite `Add local interpreter...`, et sélectionner `poetry` avec Python `3.13`).
- Attention, si vous êtes derrière un proxy, pour ce qui est de la version asynchrone en particulier, vous allez peut-être devoir passer le paramètre `proxy="http://host:port"` à la méthode `session.get()`.
### Connaissances préalables
- Connaissances de base en programmation
---
## Énoncé
### Contexte
Le téléchargement de plusieurs fichiers depuis Internet est une tâche (IO-bound) courante en développement.
Cependant, la manière dont on s'y prend peut avoir un impact considérable sur la performance et la rapidité d'exécution d'un script.
Cet exercice a pour but de comparer deux approches fondamentales : le téléchargement séquentiel (synchrone) et le téléchargement parallèle (asynchrone).
Les deux scripts Python permettent de télécharger la même liste d'images.
Le premier le fera de manière traditionnelle, une image après l'autre.
Le second utilisera les capacités asynchrones de Python pour effectuer les téléchargements en parallèle, afin d'observer le gain de performance.
---
### Partie 1 : téléchargement synchrone
Dans cette première partie, vous allez créer un script qui télécharge une liste d'images de manière séquentielle en utilisant la bibliothèque `requests`.
```mermaid
graph TD
subgraph "Processus synchrone bloc par bloc"
direction TB
A[Début] --> B{Télécharger Image 1};
B --> C[Attente Réseau 1];
C --> D{Sauvegarder Image 1};
D --> E{Télécharger Image 2};
E --> F[Attente Réseau 2];
F --> G{Sauvegarder Image 2};
G --> H[...];
H --> I{Télécharger Image N};
I --> J[Attente Réseau N];
J --> K{Sauvegarder Image N};
K --> L[Fin];
end
```
#### Objectif
À partir du squelette de code fourni, écrire un script Python nommé `sync_downloader.py` qui :
1. Télécharge une liste d'images depuis des URLs.
2. Sauvegarde ces images dans un répertoire local.
3. Mesure et affiche le temps total d'exécution.
#### Cahier des charges
1. **Configuration de base** :
* Utilisez la liste d'URLs et le chemin du répertoire de sortie fournis ci-dessous.
* Le script doit créer le répertoire de sortie s'il n'existe pas. Utilisez le module `pathlib` pour une gestion propre des chemins.
2. **Logging** :
* Mettez en place un système de `logging` simple pour afficher des informations pertinentes dans la console (début/fin du script, URL en cours de téléchargement, succès, erreurs).
* Le format des logs doit inclure l'heure, le niveau du log et le message.
3. **Logique de téléchargement** :
* Utilisez la bibliothèque `requests`. Pour optimiser les connexions réseau, effectuez toutes vos requêtes à travers une `requests.Session`.
* Créez une fonction `download_and_save` qui prend en charge le téléchargement et la sauvegarde d'une seule image.
* Parcourez la liste d'URLs et appelez cette fonction pour chaque URL.
4. **Gestion des erreurs** :
* Votre script doit être robuste. Utilisez des blocs `try...except` pour gérer les erreurs potentielles comme les erreurs réseau (`requests.exceptions.RequestException`) ou les problèmes d'écriture sur le disque (`IOError`).
* Loguez les erreurs de manière explicite sans pour autant arrêter le script (une image qui ne se télécharge pas ne doit pas empêcher les autres de se télécharger).
5. **Mesure de performance** :
* Utilisez le module `time` (par exemple, `time.perf_counter()`) pour calculer la durée totale d'exécution du processus de téléchargement.
* Affichez le résultat à la fin du script.
---
### Partie 2 : téléchargement asynchrone
Maintenant que vous avez une version fonctionnelle, mais relativement lente, vous allez la réécrire en utilisant `asyncio` pour paralléliser les téléchargements et réduire le temps d'attente.
```mermaid
graph TD
A[Début] --> B{Lancement simultané des tâches};
subgraph "Exécution parallèle gérée par la boucle asyncio"
T1[Tâche 1: Get URL 1] --> W1[Attente Réseau 1] --> S1[Sauvegarde 1];
T2[Tâche 2: Get URL 2] --> W2[Attente Réseau 2] --> S2[Sauvegarde 2];
T3[...] --> W3[...] --> S3[...];
TN[Tâche N: Get URL N] --> WN[Attente Réseau N] --> SN[Sauvegarde N];
end
B --> T1;
B --> T2;
B --> T3;
B --> TN;
S1 --> G{Fin de toutes les tâches};
S2 --> G;
S3 --> G;
SN --> G;
G --> H[Fin du script];
```
#### Objectif
À partir du squelette de code fourni, compléter le script `async_downloader.py` qui accomplit la même tâche que le précédent, mais de manière asynchrone pour une performance maximale.
#### Cahier des charges
1. **Structure asynchrone** :
* Transformez votre logique en utilisant `async` et `await`. La fonction principale (`main`) et la fonction de téléchargement (`download_and_save`) doivent devenir des coroutines (`async def`).
* Utilisez `asyncio.run(main())` pour démarrer l'exécution.
2. **Logique de téléchargement asynchrone** :
* Remplacez `requests.Session` par `aiohttp.ClientSession`.
* Dans votre fonction `download_and_save`, utilisez `session.get()` pour la requête et `response.read()` pour obtenir le contenu binaire de l'image.
* Pour l'écriture des fichiers, remplacez `open()` par `aiofiles.open()` pour ne pas bloquer la boucle événementielle.
3. **Exécution en parallèle** :
* Dans votre fonction `main`, créez une liste de "tâches" (une pour chaque image à télécharger).
* Utilisez `asyncio.gather()` pour lancer toutes les tâches en parallèle et attendre qu'elles soient toutes terminées.
4. **Gestion des erreurs et logging** :
* Adaptez la gestion des erreurs pour les exceptions spécifiques à `aiohttp` (comme `aiohttp.ClientError`).
* Conservez le système de `logging`. Notez que dans un contexte asynchrone, les messages de log des différentes tâches peuvent s'entremêler, ce qui est normal.
5. **Mesure de performance** :
* Mesurez à nouveau le temps d'exécution avec `time.perf_counter()`.
* Comparez ce temps avec celui obtenu dans la Partie 1. Qu'observez-vous ?
* Si vous avez le temps, essayez de télécharger un plus grand nombre d'images, provenant éventuellement de serveurs HTTP différents, pour voir l'impact de l'asynchronisme.
#### Question de réflexion
Pourquoi l'approche asynchrone est-elle efficace pour ce type de tâche (opérations d'entrée/sortie réseau) par rapport à l'approche synchrone ?