Adloun

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 :

Définition 10.1Modularité

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.

Définition 10.2Fonction

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.

Exemple 10.3Une fonction simple

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.

Proposition 10.4Portée des variables

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é.

Définition 10.5Module

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
Exemple 10.6Les différentes formes d'import

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

Définition 10.7Bibliothèque

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.

Exemple 10.8Utiliser la bibliothèque standard

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

Définition 10.9Spécification

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).
Définition 10.10Docstring

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__.

Exemple 10.11Une docstring complète

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

Définition 10.12Précondition

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.

Exemple 10.13Documenter une précondition

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

Définition 10.14Jeu 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

Définition 10.15Assertion

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, &quot;message&quot;.

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.")
Proposition 10.16Assertions et préconditions

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

Définition 10.17Test unitaire

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).

Exemple 10.18Regrouper les tests dans une fonction

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

Définition 10.19Types 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.
Exemple 10.20Trois erreurs typiques

# 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

Définition 10.21Système de gestion de versions

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.

Définition 10.22Dépôt, commit, branche
  • 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 init ou git clone : créer ou récupérer un dépôt.
  • git add fichier : sélectionner les modifications à enregistrer.
  • git commit -m &quot;message&quot; : 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).
Proposition 10.23Bonnes pratiques du travail collaboratif
  • 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.

Continuer sur Adloun : animation, QCM, fiches, exercices