Modularité, mise au point et tests
Cours complet · NSI (terminale), chapitre 10 · terminale, spécialité numérique et sciences informatiques
Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre
Écrire un programme qui fonctionne est une chose ; écrire un programme fiable, lisible et maintenable en est une autre. Lorsqu'un projet grossit, il devient impossible de tout placer dans un seul fichier monolithique : on découpe alors le code en modules et en fonctions réutilisables. Mais découper ne suffit pas : encore faut-il documenter ce que fait chaque fonction, vérifier qu'elle se comporte comme prévu grâce à des jeux de tests, et corriger les erreurs qui apparaissent inévitablement.
Ce chapitre présente les outils et les méthodes du développeur professionnel : la modularité (fonctions, modules, imports, bibliothèques), la documentation (docstring, spécification, préconditions), les jeux de tests et les assertions, les types d'erreurs et les techniques de débogage, ainsi que les pratiques de gestion de projet et de travail collaboratif fondées sur le versionnage.
10.1 La modularité
10.1.1 Pourquoi découper un programme ?
Un programme bien conçu est constitué de briques indépendantes, chacune réalisant une tâche précise. Cette organisation présente plusieurs avantages :
- Réutilisabilité : une fonction écrite une fois peut être appelée autant de fois que nécessaire, dans plusieurs programmes.
- Lisibilité : un découpage en sous-problèmes rend le code plus facile à comprendre.
- Maintenabilité : corriger ou améliorer une fonctionnalité se fait à un seul endroit.
- Testabilité : chaque brique peut être testée isolément.
La modularité est le principe de conception qui consiste à décomposer un programme en unités autonomes et réutilisables : les fonctions, regroupées au sein de modules, eux-mêmes parfois regroupés en bibliothèques (ou paquets).
10.1.2 Les fonctions
La fonction est l'unité de base de la modularité. Elle reçoit des paramètres, effectue un traitement et renvoie un résultat grâce à l'instruction return.
Une fonction est un bloc de code nommé, qui peut recevoir des arguments en entrée et renvoyer une valeur en sortie. Elle encapsule un traitement de manière à pouvoir être appelée par son nom.
def aire_rectangle(longueur, largeur):
return longueur * largeur
surface = aire_rectangle(5, 3)
print(surface) # affiche 15
La fonction aire_rectangle prend deux paramètres et renvoie leur produit. Une fois définie, elle peut être réutilisée partout dans le programme.
Une variable définie à l'intérieur d'une fonction est locale : elle n'existe que pendant l'exécution de la fonction et n'est pas accessible à l'extérieur. Les variables définies au niveau principal du programme sont globales. Cette séparation évite les effets de bord et rend les fonctions indépendantes les unes des autres.
10.1.3 Les modules
Lorsqu'un ensemble de fonctions traite d'un même sujet, on les regroupe dans un module, c'est-à-dire un fichier .py dédié.
Un module est un fichier source Python (extension .py) contenant des définitions (fonctions, constantes, classes) destinées à être réutilisées par d'autres programmes via le mécanisme d'import.
Méthode : Créer et importer un module
- On écrit les fonctions dans un fichier, par exemple
geometrie.py. - Dans un autre fichier situé dans le même dossier, on importe le module.
- On accède aux fonctions du module par la notation
nom_module.nom_fonction.
# fichier geometrie.py
def aire_cercle(rayon):
return 3.14159 * rayon ** 2
# fichier principal.py
import geometrie
print(geometrie.aire_cercle(2)) # 12.56636
import math # importe tout le module
print(math.sqrt(16)) # 4.0
from math import sqrt # importe une fonction precise
print(sqrt(25)) # 5.0
from math import pi as PI # importe en renommant
print(PI) # 3.141592653589793
La forme import math est la plus claire car elle indique explicitement la provenance de chaque fonction. La forme from math import * (tout importer) est déconseillée : elle masque l'origine des noms et peut provoquer des conflits.
10.1.4 Les bibliothèques
Une bibliothèque (ou librairie, ou paquet) est un ensemble de modules regroupés et distribués ensemble. La bibliothèque standard de Python (modules math, random, os, datetime…) est livrée avec l'interpréteur. D'autres bibliothèques (numpy, matplotlib, requests…) s'installent séparément, généralement avec l'outil pip.
import random
def lancer_de():
"""Simule le lancer d'un de a six faces."""
return random.randint(1, 6)
print(lancer_de()) # un entier entre 1 et 6
Réutiliser une bibliothèque éprouvée évite de réinventer des fonctionnalités et réduit les risques d'erreurs.
10.2 La documentation
Un code sans documentation est difficile à réutiliser : que fait cette fonction ? quels arguments attend-elle ? que renvoie-t-elle ? La documentation répond à ces questions.
10.2.1 Spécification et docstring
La spécification d'une fonction décrit ce qu'elle fait sans dire comment elle le fait. Elle précise :
- le rôle de la fonction ;
- les paramètres attendus, avec leur type ;
- la valeur renvoyée, avec son type ;
- les préconditions (hypothèses sur les arguments).
Une docstring (chaîne de documentation) est une chaîne de caractères placée juste après l'en-tête d'une fonction (ou d'un module). Délimitée par trois guillemets, elle constitue la documentation intégrée, accessible via la fonction help() ou l'attribut __doc__.
def valeur_absolue(x):
"""Renvoie la valeur absolue d'un nombre.
Parametre :
x (int ou float) : un nombre quelconque.
Renvoie :
int ou float : x si x >= 0, sinon -x.
"""
if x >= 0:
return x
return -x
help(valeur_absolue) # affiche la docstring
print(valeur_absolue.__doc__)
10.2.2 Les préconditions
Une précondition est une condition que les arguments d'une fonction doivent obligatoirement vérifier pour que la fonction se comporte correctement. Si une précondition n'est pas respectée, le résultat n'est pas garanti. Une postcondition décrit, à l'inverse, ce qui est garanti sur le résultat lorsque les préconditions sont satisfaites.
def racine_entiere(n):
"""Renvoie la partie entiere de la racine carree de n.
Precondition : n est un entier positif ou nul (n >= 0).
Postcondition : le resultat r verifie r*r <= n < (r+1)*(r+1).
"""
r = 0
while (r + 1) ** 2 <= n:
r += 1
return r
La précondition doit être garantie par l'appelant : la racine carrée d'un nombre négatif n'a pas de sens dans cette fonction.
10.3 Jeux de tests et assertions
Documenter ne prouve pas qu'un code est correct. Pour cela, on le confronte à des jeux de tests.
10.3.1 Notion de jeu de tests
Un jeu de tests est un ensemble de cas d'utilisation pour lesquels on connaît le résultat attendu. On distingue notamment :
- les cas généraux (situations courantes) ;
- les cas limites (valeurs extrêmes : 0, liste vide, premier ou dernier élément) ;
- les cas d'erreur (entrées invalides).
Un bon jeu de tests couvre la plus grande variété possible de situations.
10.3.2 L'instruction assert
Une assertion est une instruction assert condition qui vérifie qu'une condition est vraie. Si la condition est vraie, le programme continue normalement ; si elle est fausse, une exception AssertionError est levée et le programme s'arrête. On peut ajouter un message : assert condition, "message".
Méthode : Tester une fonction avec assert
On écrit, pour chaque cas du jeu de tests, une assertion comparant le résultat obtenu au résultat attendu. Si toutes les assertions passent silencieusement, la fonction réussit ses tests.
def maximum(a, b):
return a if a > b else b
# Jeu de tests
assert maximum(3, 5) == 5 # cas general
assert maximum(7, 2) == 7 # cas general
assert maximum(4, 4) == 4 # cas limite : egalite
assert maximum(-1, -8) == -1 # cas avec negatifs
print("Tous les tests passent.")
L'instruction assert sert aussi à vérifier les préconditions à l'entrée d'une fonction. Cela permet de détecter rapidement un appel incorrect plutôt que d'obtenir un résultat erroné difficile à diagnostiquer.
def racine_entiere(n):
assert n >= 0, "n doit etre positif ou nul"
r = 0
while (r + 1) ** 2 <= n:
r += 1
return r
10.3.3 Les tests unitaires
Un test unitaire vérifie le bon fonctionnement d'une unité de code (en général une fonction) de manière isolée. On regroupe souvent les tests d'une fonction dans une fonction de test dédiée, ce qui permet de relancer l'ensemble des vérifications après chaque modification (on parle de tests de non-régression).
def est_pair(n):
return n % 2 == 0
def test_est_pair():
assert est_pair(0) == True
assert est_pair(4) == True
assert est_pair(7) == False
assert est_pair(-2) == True
print("test_est_pair : OK")
test_est_pair()
Python propose également le module unittest et la bibliothèque pytest pour automatiser l'exécution de nombreux tests, mais le principe reste le même.
10.4 Types d'erreurs et débogage
10.4.1 Les trois grandes familles d'erreurs
On distingue trois familles d'erreurs :
- Les erreurs de syntaxe (SyntaxError) : le code n'est pas un programme Python valide ; il ne peut même pas démarrer.
- Les erreurs d'exécution (exceptions) : le programme démarre mais s'interrompt en cours de route (
ZeroDivisionError,IndexError,TypeError,ValueError…). - Les erreurs logiques (ou sémantiques) : le programme s'exécute sans planter mais produit un résultat faux. Ce sont les plus sournoises, car seul un jeu de tests permet de les détecter.
# Erreur de syntaxe : parenthese manquante
# print("bonjour"
# Erreur d'execution : division par zero
def moyenne(notes):
return sum(notes) / len(notes)
# moyenne([]) -> ZeroDivisionError
# Erreur logique : la fonction ne renvoie pas ce qu'on croit
def double(x):
return x + 2 # devrait etre x * 2 !
print(double(5)) # affiche 7 au lieu de 10
10.4.2 Gérer les exceptions
Méthode : Le bloc try / except
Pour éviter qu'une exception n'arrête brutalement le programme, on l'intercepte avec try ... except.
def moyenne(notes):
try:
return sum(notes) / len(notes)
except ZeroDivisionError:
return None # liste vide : pas de moyenne
print(moyenne([12, 14, 16])) # 14.0
print(moyenne([])) # None
10.4.3 Techniques de débogage
Méthode : Déboguer méthodiquement
- Lire le message d'erreur : la dernière ligne du traceback indique le type et la cause de l'erreur, ainsi que le numéro de ligne.
- Afficher des valeurs intermédiaires avec
print()pour suivre l'évolution des variables. - Isoler le problème en testant les fonctions une à une.
- Utiliser un débogueur (mode pas-à-pas, points d'arrêt) pour observer l'exécution en détail.
- Reproduire l'erreur avec un cas de test minimal.
10.5 Gestion de projet et travail collaboratif
10.5.1 Le versionnage
Un système de gestion de versions (ou logiciel de versionnage) enregistre l'historique des modifications apportées aux fichiers d'un projet. Il permet de revenir à un état antérieur, de comparer des versions, et de travailler à plusieurs sur le même code. Le plus répandu est Git.
- Un dépôt (repository) contient le projet et tout son historique.
- Un commit est un instantané enregistré du projet, accompagné d'un message décrivant la modification.
- Une branche est une ligne de développement parallèle, qui permet d'expérimenter sans perturber la version principale.
Méthode : Cycle de travail avec Git
git initougit clone: créer ou récupérer un dépôt.git add fichier: sélectionner les modifications à enregistrer.git commit -m "message": enregistrer un instantané.git push/git pull: envoyer ou récupérer les modifications sur un dépôt distant (par exemple GitHub ou GitLab).
- Faire des commits fréquents, petits et avec des messages clairs.
- Documenter le projet (fichier
README, docstrings). - Utiliser des branches pour les nouvelles fonctionnalités.
- Relire le code des autres et résoudre les conflits de fusion avec soin.
- Maintenir un jeu de tests pour détecter les régressions.