É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.
Une précondition est une condition qui doit être vérifiée au début de l'exécution de la fonction afin que celle-ci fonctionne correctement.
Une postcondition est une condition qui doit être vérifiée à la fin de l'exécution d'une fonction, sur son résultat, afin de s'assurer qu'il corresponde aux attentes et n'introduise pas d'erreur dans la suite du programme.
Dans notre exemple, on peut définir trois préconditions :
Python, comme d'autres langages, propose un mécanisme d'assertion dont la syntaxe est :
assert condition, "message d'erreur"
L'instruction assert
teste si une condition est
satisfaite. Si c'est le cas, elle ne fait rien. Sinon elle arrête immédiatement l'exécution
du programme et affiche le message d'erreur (qui est facultatif).
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
assert type(bits)==int and bits>=0, "bits doit être un entier naturel"
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 :
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 type(bits)==int and bits>=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
# Postconditions
assert all(c in "01" for c in binaire), "N'est pas un mot binaire."
assert len(binaire)==bits, "La longueur du nombre binaire est incorrecte."
return binaire
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.
def dec_vers_bin(n, bits):
"""Convertit le nombre n en binaire sur un nombre de bits.
Si n ou bits ne sont pas des entiers naturels ou que bits n'est pas assez grand pour réaliser la conversion,
la fonction retourne -1."""
if (type(n)!=int or n<0 or type(bits)!=int or bits<0 or bits==0 or n>=2**(bits)):
return -1
binaire = ""
for a in range(bits):
binaire = str(n%2) + binaire
n = n//2
# Postconditions
assert all(c in "01" for c in binaire), "N'est pas un mot binaire."
assert len(binaire)==bits, "La longueur du nombre binaire est incorrecte."
return binaire
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.
dec_vers_bin
.
assert dec_vers_bin(0, 4) == "0000"
assert dec_vers_bin(0, 4) == "0000"
assert dec_vers_bin(3, 4) == "0011"
assert dec_vers_bin(127, 7) == "1111111"
assert dec_vers_bin(128, 8) == "10000000"
assert dec_vers_bin(315, 10) == "0100111011"
assert dec_vers_bin(127, 4) == -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)
Remarques :
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..."
.
def afficher_heure(h, m):
return f"{h}H{m}"
def afficher_heure(h, m):
"""Renvoie l'heure sous le format "{h}H{m}"
avec h et m respectivement l'heure et les minutes données en argument."""
return f"{h}H{m}"
def afficher_heure(h, m):
"""Renvoie l'heure sous le format "{h}H{m}"
avec h et m respectivement l'heure et les minutes données en argument.
h est un entier compris entre 0 et 23 inclus.
m est un entier compris entre 0 et 59 inclus."""
assert isinstance(h, int) and 0<=h<24
assert isinstance(m, int) and 0<=m<60
return f"{h}H{m}"
def afficher_heure(h, m):
"""Renvoie l'heure sous le format "{h}H{m}"
avec h et m respectivement l'heure et les minutes données en argument.
h est un entier compris entre 0 et 23 inclus.
m est un entier compris entre 0 et 59 inclus."""
if not (isinstance(h,int) and 0<=h<24) or not (isinstance(m,int) and 0<=m<60):
help(afficher_heure)
return
return f"{h}H{m}"
assert fonct(4) == 2
assert fonct(16) == 4
assert fonct(25) == 5
assert fonct(0) == 0
assert fonct(-1) == None
En anglais :
Bibliographie :