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

7.3 KiB
Raw Blame History

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.

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.

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 ?