Exercices : modularité

Exercice 1. QCM d’activation

Pour chaque question, une seule réponse est correcte.

Question 1. On dispose d’un fichier outils.py contenant une fonction carre(n). Pour utiliser cette fonction depuis un autre fichier Python situé dans le même répertoire, on écrit :

  • A) include outils
  • B) import outils puis outils.carre(5)
  • C) import carre from outils
  • D) outils.carre(5) directement, sans import
Correction

Réponse B. L’instruction import outils rend le module accessible, puis outils.carre(5) appelle la fonction avec la notation pointée. On peut aussi écrire from outils import carre pour utiliser carre(5) directement.

  • A est faux : include n’existe pas en Python (c’est un mot-clé du C) ;
  • C est faux : la syntaxe correcte est from outils import carre (pas import ... from) ;
  • D est faux : sans import, Python ne sait pas où trouver outils.

Question 2. Une docstring est :

  • A) un commentaire commençant par #
  • B) une chaîne de caractères placée en première ligne d’une fonction ou d’un module, servant de documentation
  • C) un fichier texte accompagnant le module
  • D) une variable spéciale contenant le nom du module
Correction

Réponse B. La docstring est une chaîne (généralement entre triples guillemets """...""") placée comme première instruction d’une fonction, classe ou module. Elle est accessible via help() ou l’attribut __doc__.

  • A est faux : les commentaires # sont ignorés par Python et ne sont pas des docstrings ;
  • C est faux : la docstring est dans le code Python, pas dans un fichier séparé ;
  • D est faux : le nom du module est dans __name__, pas dans la docstring.

Question 3. Le module doctest permet de :

  • A) générer automatiquement la documentation d’un module
  • B) vérifier que les exemples écrits dans les docstrings produisent le résultat attendu
  • C) créer des tests unitaires dans un fichier séparé
  • D) corriger automatiquement les erreurs dans les docstrings
Correction

Réponse B. doctest lit les exemples au format >>> ... dans les docstrings et les exécute pour vérifier que la sortie correspond au résultat indiqué. C’est un moyen simple d’écrire des tests intégrés à la documentation.

  • A est faux : doctest ne génère pas de documentation, il teste les exemples existants ;
  • C est faux : les exemples de doctest sont dans les docstrings, pas dans un fichier séparé (contrairement à unittest) ;
  • D est faux : doctest signale les erreurs mais ne les corrige pas.

Question 4. Quelle est la différence entre import math et from math import sqrt ?

  • A) Il n’y a aucune différence
  • B) Avec import math, on écrit math.sqrt(4) ; avec from math import sqrt, on écrit sqrt(4)
  • C) from math import sqrt est plus lent car il charge tout le module
  • D) import math n’importe que la fonction math, pas le module entier
Correction

Réponse B. import math importe le module entier : on accède aux fonctions par math.sqrt, math.pi, etc. from math import sqrt n’importe que sqrt dans l’espace de noms courant, ce qui permet d’écrire sqrt(4) directement.

  • A est faux : l’accès aux fonctions diffère (notation pointée ou non) ;
  • C est faux : dans les deux cas, Python charge le module en mémoire ;
  • D est faux : import math importe bien le module math.

Question 5. Un paquet (package) Python est :

  • A) un fichier .py contenant plusieurs fonctions
  • B) un répertoire contenant un fichier __init__.py et un ou plusieurs modules
  • C) un fichier compressé .zip contenant du code Python
  • D) une bibliothèque externe installée avec pip
Correction

Réponse B. Un paquet est un répertoire qui contient un fichier __init__.py (éventuellement vide) et des modules .py. Cela permet d’organiser un projet en sous-modules avec une importation hiérarchique comme from mon_paquet.module import fonction.

  • A est faux : c’est la définition d’un module, pas d’un paquet ;
  • C est faux : un .zip peut contenir du code, mais ce n’est pas la définition d’un paquet ;
  • D est faux : pip installe des paquets, mais un paquet est défini par sa structure de répertoire.

Exercice 2. Utiliser un module (guidé)

On dispose du module geometrie.py vu en cours, contenant les fonctions aire_cercle(rayon), perimetre_cercle(rayon) et aire_rectangle(longueur, largeur).

  1. Écrire un programme qui importe le module geometrie et affiche l’aire et le périmètre d’un cercle de rayon 7.

  2. Réécrire le même programme en n’important que les fonctions aire_cercle et perimetre_cercle. Quelle syntaxe faut-il utiliser ?

  3. Que se passe-t-il si on écrit from geometrie import * ? Pourquoi cette pratique est-elle déconseillée ?

  4. Écrire l’instruction permettant d’afficher la documentation de la fonction aire_cercle depuis l’interpréteur Python.

Correction
  1. Avec import :
import geometrie

print(geometrie.aire_cercle(7))       # 153.93804002589985
print(geometrie.perimetre_cercle(7))  # 43.982297150257104
  1. Avec from ... import :
from geometrie import aire_cercle, perimetre_cercle

print(aire_cercle(7))       # 153.93804002589985
print(perimetre_cercle(7))  # 43.982297150257104

La syntaxe from module import fonction1, fonction2 permet d’importer uniquement les fonctions nécessaires et de les utiliser sans préfixe.

  1. from geometrie import * importe toutes les fonctions du module dans l’espace de noms courant. C’est déconseillé car cela peut provoquer des conflits de noms : si deux modules définissent une fonction aire, la seconde importation écrase la première silencieusement. De plus, on ne sait plus d’où vient chaque fonction en lisant le code.

  2. Pour afficher la documentation :

import geometrie
help(geometrie.aire_cercle)

Cela affiche la docstring de la fonction, qui décrit son rôle et donne des exemples.

Exercice 3. Écrire et tester des docstrings (guidé)

  1. Compléter les docstrings de la fonction suivante en ajoutant une description et deux exemples testables avec doctest :
def est_palindrome(mot):
    """
    ___________________________________

    >>> est_palindrome("kayak")
    ____
    >>> est_palindrome("python")
    ____
    """
    return mot == mot[::-1]
  1. Écrire une fonction pgcd(a, b) qui calcule le PGCD de deux entiers par l’algorithme d’Euclide. Ajouter une docstring complète avec au moins trois exemples, dont un cas limite (par exemple pgcd(0, 5)).

  2. Écrire le code permettant de lancer les tests doctest sur ces deux fonctions.

Correction
  1. Docstring complétée :
def est_palindrome(mot):
    """Renvoie True si mot est un palindrome, False sinon.

    >>> est_palindrome("kayak")
    True
    >>> est_palindrome("python")
    False
    """
    return mot == mot[::-1]
  1. Fonction pgcd documentée :
def pgcd(a, b):
    """Renvoie le PGCD de a et b par l'algorithme d'Euclide.

    >>> pgcd(48, 18)
    6
    >>> pgcd(17, 5)
    1
    >>> pgcd(0, 5)
    5
    """
    while b != 0:
        a, b = b, a % b
    return a

Pour pgcd(0, 5) : la boucle s’exécute une seule fois (a, b devient 5, 0), puis on renvoie 5. C’est cohérent : tout entier divise 0, donc le PGCD de 0 et 5 est 5.

  1. Lancement des tests :
import doctest
doctest.testmod()

Si les fonctions sont dans un module mes_fonctions.py, on peut aussi lancer depuis le terminal :

python3 -m doctest mes_fonctions.py -v

L’option -v (verbose) affiche le détail de chaque test, même ceux qui réussissent.

Exercice 4. Créer un module de statistiques

On souhaite créer un module stats.py contenant les fonctions suivantes, chacune documentée avec une docstring et des exemples doctest :

  1. moyenne(liste) : retourne la moyenne arithmétique d’une liste de nombres.

  2. variance(liste) : retourne la variance de la liste, définie par $\frac{1}{n}\sum_{i=1}^{n}(x_i - \bar{x})^2$ où $\bar{x}$ est la moyenne.

  3. ecart_type(liste) : retourne l’écart type (racine carrée de la variance).

  4. mediane(liste) : retourne la médiane d’une liste triée. Si la liste a un nombre pair d’éléments, la médiane est la moyenne des deux éléments centraux.

  5. Ajouter en fin de fichier le bloc suivant et expliquer son rôle :

if __name__ == "__main__":
    import doctest
    doctest.testmod()
Correction
"""Module de calculs statistiques de base."""

import math


def moyenne(liste):
    """Renvoie la moyenne arithmétique d'une liste de nombres.

    >>> moyenne([10, 20, 30])
    20.0
    >>> moyenne([4])
    4.0
    """
    return sum(liste) / len(liste)


def variance(liste):
    """Renvoie la variance de la liste.

    >>> variance([10, 20, 30])
    66.66666666666667
    >>> variance([5, 5, 5])
    0.0
    """
    m = moyenne(liste)
    return sum((x - m) ** 2 for x in liste) / len(liste)


def ecart_type(liste):
    """Renvoie l'écart type de la liste.

    >>> round(ecart_type([10, 20, 30]), 2)
    8.16
    >>> ecart_type([5, 5, 5])
    0.0
    """
    return math.sqrt(variance(liste))


def mediane(liste):
    """Renvoie la médiane d'une liste de nombres.

    >>> mediane([3, 1, 2])
    2
    >>> mediane([4, 1, 3, 2])
    2.5
    """
    tri = sorted(liste)
    n = len(tri)
    if n % 2 == 1:
        return tri[n // 2]
    else:
        return (tri[n // 2 - 1] + tri[n // 2]) / 2


if __name__ == "__main__":
    import doctest
    doctest.testmod()

Explication du bloc if __name__ == "__main__" : quand on exécute directement le fichier (python3 stats.py), la variable __name__ vaut "__main__" et les tests sont lancés. En revanche, quand on importe le module depuis un autre fichier (import stats), __name__ vaut "stats" et les tests ne s’exécutent pas automatiquement. Ce mécanisme permet de distinguer l’exécution directe (pour tester) de l’importation (pour utiliser).

Exercice 5. Structurer un projet (synthèse)

On souhaite organiser un petit projet de gestion de notes en trois modules, regroupés dans un paquet gestion_notes/.

  1. Proposer une structure de fichiers pour ce paquet, avec les modules suivants :

    • eleve.py : contient une classe Eleve avec un nom et une liste de notes ;
    • calculs.py : contient les fonctions moyenne(notes) et meilleure_note(notes) ;
    • affichage.py : contient une fonction bulletin(eleve) qui affiche le nom, la moyenne et la meilleure note.
  2. Écrire le contenu du fichier eleve.py avec une classe Eleve dotée d’un constructeur (__init__), d’une méthode ajouter_note(note) et d’une méthode __repr__. Documenter chaque méthode.

  3. Écrire le contenu du fichier calculs.py avec les deux fonctions documentées.

  4. Écrire le contenu du fichier affichage.py. Ce module devra importer les fonctions moyenne et meilleure_note de calculs.py.

  5. Écrire un fichier main.py (en dehors du paquet) qui crée deux élèves, leur ajoute des notes, puis affiche leur bulletin.

Correction
  1. Structure du projet :
gestion_notes/
├── __init__.py
├── eleve.py
├── calculs.py
└── affichage.py
main.py

Le fichier __init__.py peut être vide. Il signale à Python que le répertoire gestion_notes est un paquet.

  1. Fichier eleve.py :
"""Module de gestion des élèves."""


class Eleve:
    """Représente un élève avec son nom et ses notes."""

    def __init__(self, nom):
        """Initialise un élève avec un nom et une liste de notes vide.

        >>> e = Eleve("Alice")
        >>> e.nom
        'Alice'
        >>> e.notes
        []
        """
        self.nom = nom
        self.notes = []

    def ajouter_note(self, note):
        """Ajoute une note à la liste de l'élève.

        >>> e = Eleve("Alice")
        >>> e.ajouter_note(15)
        >>> e.ajouter_note(12)
        >>> e.notes
        [15, 12]
        """
        self.notes.append(note)

    def __repr__(self):
        """Renvoie une représentation lisible de l'élève.

        >>> Eleve("Alice")
        Eleve('Alice', notes=[])
        """
        return f"Eleve('{self.nom}', notes={self.notes})"
  1. Fichier calculs.py :
"""Module de calculs sur les notes."""


def moyenne(notes):
    """Renvoie la moyenne d'une liste de notes.

    >>> moyenne([12, 14, 16])
    14.0
    >>> moyenne([20])
    20.0
    """
    return sum(notes) / len(notes)


def meilleure_note(notes):
    """Renvoie la meilleure note de la liste.

    >>> meilleure_note([12, 14, 16])
    16
    >>> meilleure_note([8])
    8
    """
    return max(notes)
  1. Fichier affichage.py :
"""Module d'affichage des bulletins."""

from gestion_notes.calculs import moyenne, meilleure_note


def bulletin(eleve):
    """Affiche le bulletin d'un élève.

    Paramètre : un objet Eleve possédant les attributs nom et notes.
    """
    if not eleve.notes:
        print(f"{eleve.nom} : aucune note enregistrée.")
        return
    moy = round(moyenne(eleve.notes), 2)
    meilleure = meilleure_note(eleve.notes)
    print(f"Bulletin de {eleve.nom}")
    print(f"  Moyenne : {moy}")
    print(f"  Meilleure note : {meilleure}")
  1. Fichier main.py :
from gestion_notes.eleve import Eleve
from gestion_notes.affichage import bulletin

alice = Eleve("Alice")
alice.ajouter_note(15)
alice.ajouter_note(12)
alice.ajouter_note(18)

bob = Eleve("Bob")
bob.ajouter_note(9)
bob.ajouter_note(14)

bulletin(alice)
bulletin(bob)

Résultat :

Bulletin de Alice
  Moyenne : 15.0
  Meilleure note : 18
Bulletin de Bob
  Moyenne : 11.5
  Meilleure note : 14

Ce projet illustre les principes de modularité : chaque module a une responsabilité claire (données, calculs, affichage), les imports sont explicites, et les fonctions sont documentées. On peut tester chaque module indépendamment avec doctest.