Écrire un programme, c'est bien mais c'est encore mieux s'il est parfaitement juste et que l'on peut être sûr qu'il fonctionnera en toute circonstance. C'est ce que l'on appelle prouver un programme mais cela nécessite la maîtrise d'un langage mathématique fondé sur la logique classique, l'arithmétique et la théorie des ensembles que nous n'aborderons pas dans ce cours.
Une première étape vers ce processus de preuve d'un programme est d'apprendre à définir précisément ce dont le programme a besoin et ce qu'il doit répondre. C'est ce que l'on appelle prototyper une fonction ou bien écrire ses spécifications.
Voici une fonction écrite en Python, convertissant un nombre décimal en binaire sur un nombre de bits donné en paramètre.
def dec_vers_bin(n, bits):
binaire = ""
for a in range(bits):
binaire = str(n%2) + binaire
n = n//2
return binaire
Pour utiliser cette fonction, il est nécessaire de savoir ce qu'elle fait et sous quelles
conditions. Nous pouvons ajouter dans le corps de la fonction, des commentaires sur le bon
usage de cette fonction. C'est ce que l'on appelle documenter une fonction. C'est cette
documentation qui est renvoyée lorsque la fonction est donnée en argument de la fonction
native help()
.
def dec_vers_bin(n, bits):
"""Convertit le nombre n en binaire sur un nombre de bits."""
binaire = ""
for a in range(bits):
binaire = str(n%2) + binaire
n = n//2
return binaire
help()
sur cette fonction et vérifier son comportement.
Décrire les préconditions et les postconditions d'une fonction correspond à écrire ses spécifications :
assert condition, "message d'erreur"
assert
teste si une condition est
satisfaite.Voyons comment l'utiliser pour vérifier les préconditions pour la fonction précédente :
def dec_vers_bin(n, bits):
"""Convertit le nombre n en binaire sur un nombre de bits."""
# Préconditions
assert type(n)==int and n>=0, "n doit être un entier naturel"
assert bits!=0 and n<2**(bits), "le nombre de bits est insuffisant"
binaire = ""
for a in range(bits):
binaire = str(n%2) + binaire
n = n//2
return binaire
Le mécanisme d'assertion permet de résoudre des erreurs. En effet, en arrêtant l'exécution avant que l'erreur se produise et en la documentant, il permet de se rendre immédiatement compte d'un problème dans le programme et de le corriger.
De même on peut insérer en fin de fonction, juste avant le return
,
des assertions pour vérifier des postconditions. Dans notre exemple nous allons en écrire deux :
Ce mode de programmation qui utilise les assertions pour vérifier les préconditions et les postconditions, est appelé programmation défensive. C'est utilisé pour du code dont on a le contrôle, en cours de développement par exemple.
Lorsqu'on écrit un module, on peut programmer défensivement les fonctions qui sont destinées à être
seulement appelées au sein de ce dernier. Pour des fonctions destinées à être utilisées par
d'autres personnes, en dehors du module, on va plutôt faire une gestion active des erreurs,
notamment avec des structures conditionnelle if else
et en
prévoyant des valeurs de retour spéciales qu'il faudra documenter.
Il est alors préférable, lorsqu'on définit et spécifie une nouvelle fonction, de minimiser le nombre de préconditions si on veut la rendre robuste, c'est-à-dire résistante à de mauvaises utilisations sans qu'elle ne génère d'erreur. Elle doit alors être capable de gérer les différents mésusages. C'est particulièrement nécessaire lorsque cette fonction est vouée à être utilisée directement par l'utilisateur. Cependant, pour un usage interne à un programme, c'est moins critique car vous maîtrisez tous les appels et le code de la fonction n'en sera que plus simple, débarassée des assertions.
Il faut comprendre que le mécanisme d'assertion est une aide au développeur, et ne doit
en aucun cas faire partie du code final d'un programme. En supprimant toutes les instructions
assert
, le programme doit continuer à fonctionner normalement.
dec_vers_bin
comme si elle devait être utilisée directement par l'utilisateur.
Le développement piloté par des tests (TDD) est une méthode d'écriture de programme qui met en avant le fait d'écrire d'abord des jeux de tests pour chaque fonction du programme (dans ses spécifications) puis d'écrire le code qui permettra au programme de passer ces tests avec succès.
Cette manière de procéder permet de penser plus profondément les spécifications d'un programme, en amont de l'écriture du code. Cela :
La qualité et le nombre de tests sont importants pour atteindre la qualité souhaitée du code mais un jeu de tests ne pourra jamais être exhaustif et prouver la correction d'un programme (garantir qu'il n'y aura pas d'erreur).
Un jeu de tests doit s'attacher à vérifier tous les cas particuliers et les cas limites d'usage pour être réellement utile. Il est inutile de tester toutes les possibilités.
Voici un jeu de tests pour la fonction dec_vers_bin
. L'idée la plus
simple pour mettre en place un jeu de tests est d'utiliser des assertions pour vérifier que
les retours de la fonction correspondent à ce qui est attendus.
0
s'écrit en binaire sur 4 bits : 0000
3
s'écrit en binaire sur 4 bits : 0011
127
s'écrit en binaire sur 7 bits : 1111111
128
s'écrit en binaire sur 8 bits : 10000000
315
s'écrit en binaire sur 10 bits : 0100111011
dec_vers_bin
.
-1
.
L'inconvénient d'insérer des assertions directement après une fonction pour la tester est que celles-ci seront exécutées tout le temps, à chaque exécution du script, ce qui peut entraîner une perte de temps. Lorsque le script est importé en tant que module par un autre script, on préfère éviter ces tests. Cela est possible en utilisant une structure conditionnelle :
if __name__ == "__main__" :
assert dec_vers_bin(0, 4) == "0000"
assert dec_vers_bin(3, 4) == "0011"
Cette structure permet de vérifier que l'attribut __name__
du script
lui-même est bien égal à "__main__"
c'est-à-dire
qu'il est exécuté directement, en tant que script principal et non pas en tant que module importé par un autre script.
Ainsi les tests sont toujours utilisables
en exécutant directement le script mais ne perturbe pas l'exécution d'un projet plus large utilisant ce script comme module.
if __name__ == "__main__" :
.
Le module doctest permet d'inclure le jeu de tests à l'intérieur du corps de la fonction directement dans sa documentation. Voici le code correspondant :
def dec_vers_bin(n, bits):
"""
Convertit le nombre n en binaire sur un nombre de bits.
Jeu de tests :
>>> dec_vers_bin(0, 4)
'0000'
>>> dec_vers_bin(3, 4)
'0011'
"""
binaire = ""
for a in range(bits):
binaire = str(n%2) + binaire
n = n//2
return binaire
if __name__ == "__main__" :
import doctest
doctest.testmod(verbose=True)
verbose=True
permet d'afficher le
test même s'il est réussi. Si on veut afficher seulement les tests qui échouent
on mettra verbose=False
(valeur par défaut).
Remarque : Le module doctests est assez utile mais atteint vite ses limites lorsque les structures de données à vérifier sont plus complexes, comme avec les dictionnaires où l'ordre des éléments n'est pas garanti.
afficher_heure(h, m)
qui
prend l'heure h
et les minutes m
en argument et retourne la chaîne de caractère "...H..."
.
assert fonct(4) == 2
assert fonct(16) == 4
assert fonct(25) == 5
assert fonct(0) == 0
assert fonct(-1) == None
doctest
.
En français | en anglais |
---|---|
développement piloté par des tests | test driven developement (TDD) |
assertions | assertions |
Bibliographie :