03 Mise au point des scripts et gestion des exceptions
Table des matières
- Les bonnes pratiques : documenter les fonctions
- Les tests et les jeux de tests
- Les préconditions et les postconditions
- Les bibliothèques ou modules
- Gestion des exceptions
- Exercices
🎯 Ce que dit le programme officiel de Première NSI
Spécification — Capacités attendues :
- Prototyper une fonction ;
- Décrire les préconditions sur les arguments ;
- Décrire des postconditions sur les résultats.
Des assertions peuvent être utilisées pour garantir des préconditions ou des postconditions.
Mise au point de programmes — Capacité attendue :
- Utiliser des jeux de tests.
L'importance de la qualité et du nombre des tests est mise en évidence. Le succès d'un jeu de tests ne garantit pas la correction d'un programme.
Le programme insiste aussi sur l'importance de la spécification, de la documentation et des tests, ainsi que sur l'intérêt de la modularisation qui permet la réutilisation de programmes et la mise à disposition de bibliothèques. Pour vos programmes simples, une spécification rapide mais précise suffit.
1. Les bonnes pratiques : spécifier et documenter les fonctions
⚓︎
1.1. Prototyper et spécifier une fonction
⚓︎
Le prototype d'une fonction est sa « carte d'identité » : son nom, ses paramètres (avec leur type) et le type du résultat renvoyé.
En Python, on peut écrire le prototype directement dans l'en-tête grâce aux annotations de type :
def factorielle(n: int) -> int:
Ce prototype se lit : « la fonction factorielle prend un paramètre n de type int et renvoie un int ». ⚠️ Ces annotations sont indicatives : Python ne vérifie pas les types à l'exécution.
La spécification d'une fonction complète le prototype. Elle décrit ce que fait la fonction (son rôle), sans dire comment elle le fait. Une spécification rapide mais précise comporte :
-
le prototype (nom, paramètres, type du résultat) ;
-
le rôle de la fonction ;
-
les préconditions : conditions que doivent respecter les arguments (aussi appelées conditions d'utilisation, CU) ;
-
les postconditions : propriétés garanties sur le résultat.
Exemple de spécification
- Prototype :
factorielle(n: int) -> int - Rôle : renvoie la factorielle de
n, c'est-à-dire n! = 1 × 2 × … × n. - Précondition :
nest un entier positif ou nul (n >= 0). - Postcondition : le résultat est un entier supérieur ou égal à 1.
En Python, cette spécification s'écrit dans la docstring de la fonction : c'est l'objet de la sous-section suivante.
1.2. Qu’est-ce qu’une docstring ?
⚓︎
Une docstring est un texte placé juste après l’en-tête d’une fonction en Python. Elle sert à expliquer :
-
Ce que fait la fonction ;
-
Comment l’utiliser (informations sur les paramètres et le résultat attendu) ;
-
Les conditions d’utilisation pour éviter les erreurs.
Elle est essentielle pour rendre le code plus lisible et plus compréhensible.
1.3. Comment écrire une docstring ?
⚓︎
Activité n°1 : Ajouter une docstring
Tester :
def factorielle(n: int) -> int:
"""
Fonction qui retourne la factorielle d'un nombre.
:param n: int
:return: int
CU (conditions d'utilisation) : n >= 0
>>> factorielle(0)
1
>>> factorielle(4)
24
"""
resultat = 1
for i in range(2, n + 1):
resultat = resultat * i
return resultat
Exécuter :
help(factorielle)
Python
Solution
Résultat :
Help on function factorielle in module __main__:
factorielle(n: int) -> int
Fonction qui retourne la factorielle d'un nombre.
:param n: int
:return: int
CU (conditions d'utilisation) : n >= 0
>>> factorielle(0)
1
>>> factorielle(4)
24
Explication :
-
La docstring est affichée avec la commande
help(factorielle). -
Elle décrit clairement l’objectif et les conditions d’utilisation.
-
helpaffiche aussi le prototypefactorielle(n: int) -> int: la docstring + le prototype constituent la spécification de la fonction.
2. Les tests et les jeux de tests
⚓︎
Les tests permettent de vérifier qu’un programme ne produit pas d’erreur et qu’il effectue bien la tâche attendue.
Un jeu de tests est un ensemble de cas de test : pour chaque cas, on choisit une entrée et on calcule à l'avance le résultat attendu.
Choisir un bon jeu de tests
La qualité et le nombre des tests sont essentiels. Un bon jeu de tests couvre :
- des cas ordinaires (valeurs « normales ») ;
- des cas limites (0, 1, valeur minimale ou maximale autorisée…) ;
- des cas particuliers (valeurs négatives, décimales, mauvais type… si la spécification le permet).
2.1. Les tests simples avec assert
⚓︎
L’instruction assert est utilisée pour vérifier rapidement que le programme fonctionne correctement.
Activité n°2 : Vérifier une fonction avec assert
Tester :
def carre(x):
return x * x
assert carre(3) == 9 # Vérifie que le carré de 3 est bien 9
assert carre(0) == 0 # Vérifie que le carré de 0 est bien 0
assert carre(-2) == 4 # Vérifie que le carré de -2 est bien 4
Python
Solution
Résultat : (Aucune sortie si tout est correct !)
Explication :
-
Si tous les tests passent, le programme continue.
-
Si un test échoue, Python affiche une erreur et s’arrête.
Activité n°3 : Vérifier une division euclidienne
Tester :
def division_euclidienne(a, b):
if a >= 0 and b > 0 and type(a) == int and type(b) == int:
return a // b, a % b # Quotient et reste
else:
return -1 # Code d'erreur si les valeurs ne sont pas correctes
if __name__ == '__main__':
assert division_euclidienne(10, 2) == (5, 0)
assert division_euclidienne(-10, 7) == -1
assert division_euclidienne(10.3, 4) == -1
assert division_euclidienne(3, 0) == -1
Python
Explication :
-
assertteste plusieurs cas et arrête le programme si un test échoue. -
La condition
if __name__ == '__main__':garantit que ces tests ne s’exécutent que si le fichier est exécuté directement.
Activité n°4 : Vérifier une division euclidienne
Tester :
def division_euclidienne(a, b):
if a >= 0 and b > 0 and type(a) == int and type(b) == int:
return a // b, a % b # Quotient et reste
else:
return -1 # Code d'erreur si les valeurs ne sont pas correctes
if __name__ == '__main__':
assert division_euclidienne(10, 2) == (5, 1)
assert division_euclidienne(-10, 7) == -1
assert division_euclidienne(10.3, 4) == -1
assert division_euclidienne(3, 0) == -1
Python
Solution
Explication :
assert division_euclidienne(10, 2) == (5, 1) # Test invalide, Python montre une erreur, car le reste de 10 divisé par 2 est 0 et non 1.
2.2. Les tests avec doctest
⚓︎
doctest permet de vérifier automatiquement que les exemples donnés dans la docstring sont corrects.
Activité n°5 : Tester avec doctest
Tester :
import doctest
def factorielle(n):
"""
Fonction qui retourne la factorielle d'un nombre.
:param n: int
:return: int
CU (conditions d'utilisation) : n >= 0
>>> factorielle(0)
1
>>> factorielle(4)
24
"""
resultat = 1
for i in range(2, n + 1):
resultat = resultat * i
return resultat
doctest.testmod()
Python
Solution
Explication :
-
doctestrecherche les triples chevrons>>>dans la docstring. -
Il exécute les exemples et compare le résultat attendu et le résultat réel.
-
Si tout est correct, rien ne s’affiche.
Activité n°6 : Introduire une erreur dans doctest
Tester :
import doctest
def factorielle(n):
"""
Fonction qui retourne la factorielle d'un nombre.
:param n: int
:return: int
CU (conditions d'utilisation) : n >= 0
>>> factorielle(0)
100000 # ERREUR VOLONTAIRE
>>> factorielle(4)
24
"""
resultat = 1
for i in range(2, n + 1):
resultat = resultat * i
return resultat
doctest.testmod()
Python
Solution
Explication :
-
Doctest va signaler une erreur, car
factorielle(0)ne renvoie pas100000. -
C’est une façon rapide de vérifier si les résultats des tests sont corrects.
Activité n°7 : Activer le mode verbose dans doctest
Tester :
import doctest
def factorielle(n):
"""
Fonction qui retourne la factorielle d'un nombre.
:param n: int
:return: int
CU (conditions d'utilisation) : n >= 0
>>> factorielle(0)
1
>>> factorielle(4)
24
"""
resultat = 1
for i in range(2, n + 1):
resultat = resultat * i
return resultat
doctest.testmod(verbose=True)
Python
Solution
Explication :
-
En mode verbose, doctest affiche tous les tests, qu’ils réussissent ou échouent.
-
Utile pour voir ce qui est testé.
2.3. Les limites d'un jeu de tests
⚓︎
⚠️ À retenir (programme officiel)
Le succès d'un jeu de tests ne garantit pas la correction d'un programme.
Un test qui échoue prouve la présence d'un bug. Mais un jeu de tests qui réussit prouve seulement que le programme est correct sur les cas testés… pas sur tous les autres !
Activité n°7 bis : Des tests qui réussissent… sur une fonction fausse !
On veut programmer la fonction somme(a, b) qui renvoie la somme de a et b. Un élève propose ce code :
Tester :
def somme(a, b):
"""Renvoie la somme de a et b."""
return a * b # ERREUR : multiplication au lieu d'addition !
assert somme(2, 2) == 4
assert somme(0, 0) == 0
print("Tous les tests sont passés !")
Python
Solution
Résultat : Tous les tests sont passés !
Explication :
-
Le jeu de tests réussit alors que la fonction est fausse : par malchance,
2 * 2 == 2 + 2et0 * 0 == 0 + 0! -
Il suffit d'ajouter un cas mieux choisi pour révéler le bug :
🐍 Script Pythonassert somme(2, 3) == 5 # AssertionError : somme(2, 3) renvoie 6 -
Conclusion : le succès d'un jeu de tests ne garantit pas la correction du programme. D'où l'importance de la qualité et du nombre des tests.
3. Les préconditions et les postconditions
⚓︎
On a vu en 1.1 que la spécification d'une fonction décrit :
-
les préconditions : conditions sur les arguments, à vérifier avant le calcul ;
-
les postconditions : propriétés du résultat, garanties après le calcul.
Des assertions (assert) peuvent être utilisées pour garantir ces préconditions et postconditions : elles permettent de vérifier les résultats tout au long de l'exécution d'un programme, et pas seulement test par test.
Activité n°8 :
Tester :
from math import sqrt
def square_root(x):
"""
Fonction qui calcule la racine carrée d'un nombre positif.
:param x: int ou float (doit être >= 0)
:return: float
Précondition : x >= 0
Postcondition : Le carré du résultat doit être égal à x
"""
assert x >= 0, "Précondition non respectée : x doit être positif ou nul"
racine = sqrt(x)
assert racine**2 == x, "Postcondition non respectée : erreur dans le calcul"
return racine
print(square_root(25)) # Doit afficher 5.0
print(square_root(-25)) # Doit lever une erreur
Python
Solution
Explication :
-
Précondition
assert x >= 0: empêche le calcul sixest négatif. -
Postcondition
assert racine**2 == x: vérifie la validité du résultat. -
Si une précondition ou postcondition échoue, le programme s’arrête immédiatement avec un message d’erreur.
Pour plus de précisions : https://www.youtube.com/watch?v=DRVoh5XiAZo
4. Les bibliothèques ou modules
⚓︎
4.1. Qu’est ce que c’est ?
⚓︎
Une bibliothèque en Python est un fichier contenant du code réutilisable. Ce code peut être sous forme de fonctions, classes ou variables. Pour utiliser une bibliothèque, on doit l’importer dans notre programme.
Une bibliothèque est une collection de modules. C'est un ensemble plus large qui peut contenir plusieurs fichiers Python, chacun étant un module.
La modularisation
Découper un programme en modules s'appelle la modularisation. Son intérêt : permettre la réutilisation de programmes déjà écrits (et testés !) et la mise à disposition de bibliothèques. C'est pour cela qu'une fonction bien spécifiée et bien documentée est précieuse : on peut la réutiliser sans relire son code, en lisant seulement sa docstring.
4.2. Importer des bibliothèques (modules) standards
⚓︎
Voici un exemple d’utilisation d’une bibliothèque Python standard, le module math qui contient des fonctions mathématiques comme sqrt() (racine carrée) ou random qui contient randint.
On peut noter les 2 manières d’importer le module :
-
import
nom_du_module -
from
nom_du_moduleimportnom_de_la_fonction
>>> import math as m
>>> m.sqrt(25)
>>> import math
>>> math.sqrt(25)
>>> from math import sqrt
>>> sqrt(25)
>>> import random
>>> x = random.randint(0,10)
>>> from random import randint
>>> randint(0,10)
>>> from random import *
>>> randint(0,10)
>>> randrange(10,20,3) #retourne un entier entre 10 et 19 et un pas =3
Activité n°9 :
Tester :
import math
import random
print(math.sqrt(25)) # Racine carrée de 25
x = random.randint(0, 10) # Génère un nombre aléatoire entre 0 et 10
print("Nombre aléatoire :", x)
Python
Solution
Explication :
-
import mathpermet d’accéder aux fonctions mathématiques. -
import randompermet d'utiliser des fonctions pour générer des nombres aléatoires.
5. Gestion des exceptions
⚓︎
Ce sont les erreurs que peut rencontrer Python en exécutant le programme. Ces erreurs peuvent être interceptées très facilement et dans certains cas indispensables.
5.1. Lever d’exception
⚓︎
Une exception est une erreur qui se produit lors de l’exécution du programme. Si elle n'est pas gérée, le programme s’arrête brutalement.
Activité n°10 :
Tester :
print(1 / 0) # Division par zéro
Python
Solution
Explication :
- Python lève une exception
ZeroDivisionError, car on ne peut pas diviser par zéro.
5.2. Gestions des exceptions
⚓︎
On peut éviter l'arrêt brutal d'un programme en capturant les erreurs avec try et except.
5.2.1. Forme minimale du bloc try
⚓︎
Activité n°11 : Gestion des erreurs d’entrée utilisateur
annee = input('Entrer une année : ')
try:
annee = int(annee) # Convertit l'entrée en entier
print("Pas de problème :", annee)
except ValueError:
print("Erreur : Vous devez entrer un nombre entier.")
Python
Solution
Explication :
-
Si l’utilisateur entre un nombre, tout fonctionne.
-
Si l’utilisateur entre du texte, une erreur est capturée et un message explicite s’affiche.
5.2.2. Interception d’exceptions particulières
Dans le cas d’une division :⚓︎
On peut capturer différents types d’erreurs séparément.
Activité n°12 : Gérer plusieurs erreurs
numerateur = input('Numérateur ? ')
denominateur = input('Dénominateur ? ')
try:
resultat = int(numerateur) / int(denominateur)
except ValueError:
print("Erreur : Vous devez entrer des nombres.")
except ZeroDivisionError:
print("Erreur : Division par zéro impossible.")
else:
print("Le résultat est :", resultat)
Python
Solution
Explication :
-
ValueError: L'utilisateur a saisi un texte au lieu d’un nombre. -
ZeroDivisionError: L’utilisateur a essayé de diviser par zéro. -
else: S’exécute uniquement si aucunexceptn’a été déclenché.
5.2.3. finally : Toujours exécuté
⚓︎
finally : Toujours exécutéActivité n°13 : Utilisation de finally
numerateur = input('Numérateur ? ')
denominateur = input('Dénominateur ? ')
try:
resultat = int(numerateur) / int(denominateur)
except ValueError:
print("Erreur : Vous devez entrer des nombres.")
except ZeroDivisionError:
print("Erreur : Division par zéro impossible.")
else:
print("Le résultat est :", resultat)
finally:
print("Fin de l’exécution du programme.")
Python
Solution
Explication :
finallyaffiche un message final, que le programme ait réussi ou non.
5.2.4. Lever une exception avec raise
⚓︎
raiseOn peut déclencher une exception volontairement si une valeur est incorrecte.
Activité n°14 : Déclencher une erreur volontairement
annee = input("Saisissez une année supérieure à 0 : ")
try:
annee = int(annee)
if annee >= 3000 or annee < 0:
raise ValueError("L'année doit être comprise entre 0 et 2999.")
except ValueError as e:
print("Erreur :", e)
Python
Solution
Explication :
-
Si l’utilisateur entre
3000, l’exception est déclenchée volontairement. -
Le message d’erreur est personnalisé.
6. Exercices
⚓︎
🧠 Exercices dans Capytale => le code vous sera donné par votre enseignant
Exercice 1
On considère la fonction multiplier_par_deux(x) qui prend en paramètre x et qui renvoie son double. Ecrire un script de cette fonction avec :
-
Sa documentation dans le docstring comme dans le cours. Ne pas mettre d’exemples
-
4 tests en assert : avec 0, avec -1, avec 3.2 et avec ‘a’
Exercice 2
On considère une fonction somme_carres(x) qui prend en paramètre x (entier strictement positif) et renvoie la somme des x premiers carrés non nuls.
Ecrire un script de cette fonction avec :
-
Sa documentation dans le docstring comme dans l’activité 1. Ne pas mettre d’exemples
-
3 tests en assert (comme dans l’activité 3) : avec 1, avec 2 et avec 3
Exercice 3
La fonction précédente à pour condition d’utilisation : x doit être strictement positif. Déclencher une exception et capturer là si le nombre entrée est 0 ou négatif.
Exemple :
>>> somme_carres(5)
55
>>> somme_carres(0)
Le nombre entré est nul ou négatif
>>> somme_carres(-2)
Le nombre entré est nul ou négatif
Exercice 4
On part du script suivant qui permet d’inverser un nombre
try:
chaine = input('Entrer un nombre : ') # permet de pouvoir gérer l’exception ValueError
nombre = float(chaine)
inverse = 1.0/nombre
except ValueError:
#ce bloc est exécuté si une exception de type ValueError est levée dans le bloc try
print(chaine, "n'est pas un nombre !")
except ZeroDivisionError:
#ce bloc est exécuté si une exception de type ZeroDivisionError est levée dans le bloc try
print("Division par zéro !")
else:
#on arrive ici si aucune exception n'est levée dans le bloc try
print("L'inverse de", nombre, "est :", inverse)
Compléter le script précédent de manière à ressaisir le nombre en cas d’erreur. On pourra englober le script dans une boucle while. Par exemple :
Tester avec :
-
'a'
-
relancer le programme et tester avec => 'salut !'
-
relancer le programme et tester avec => 0
-
relancer le programme et tester avec => 2
Aide : penser à une boucle infinie et au mot clé break.
Exercice 5
Ecrire un script qui calcule la racine carrée d’un nombre, avec gestion des exceptions. Utiliser la fonction sqrt() du module math.
Par exemple :
Entrer un nombre : >? go
go n'est pas un nombre valide !
Entrer un nombre : >? -5.26
-5.26 n'est pas un nombre valide !
Entrer un nombre : >? 16
La racine de 16.0 est : 4.0
Tester avec :
-
"go"
-
relancer le programme et tester avec => -5.26
-
relancer le programme et tester avec => 16
Remarquez bien qu’on demande le nombre à l’utilisateur jusqu'à ce qu’il convienne.
Exercice 6
Un élève a écrit la fonction suivante, censée renvoyer le maximum de deux nombres :
def maximum(a, b):
"""Renvoie le plus grand des deux nombres a et b."""
return abs(a)
Il la valide avec le jeu de tests suivant, qui réussit :
assert maximum(5, 3) == 5
assert maximum(-2, -7) == 2 # Oups... ce résultat attendu est-il correct ?
-
Donner le prototype de la fonction
maximum(avec les annotations de type). -
Le deuxième cas de test est mal conçu : le résultat attendu est faux. Quel est le bon résultat attendu pour
maximum(-2, -7)? Que se passe-t-il alors ? -
Proposer un jeu de 4 tests bien choisis (cas ordinaire, cas limite
a == b, cas négatifs) qui révèle le bug. -
Corriger la fonction, puis vérifier que tout le jeu de tests réussit.
-
Ce succès garantit-il que la fonction est correcte ?