Outils et méthodes de développement
Vous avez déjà commencé à faire des programmes un peu compliqués. Et vous avez sans doute eu des erreurs un peu compliquées à comprendre et à résoudre.
Ce chapitre a pour objectif de vous présenter quelques outils et méthodes pour éviter un certain nombre d'erreurs dans votre code.
Postcondition : doctest
Souvent, pour vérifier qu'une fonction, par exemple maximum, qui calcule le maximum d'une liste, faut bien ce qu'on lui demande (la postcondition), vous faites des print et vous vérifiez le résultat à la main:
def maximum(liste):
# ici du code mais c'est pas important
print(maximum([1, 2, 3, 5, 2]))
print(maximum([-1, -2, -5, -3]))
print(maximum([-1, 2, 3, -6]))
Si le programme affiche
C'est bon !
Déjà, félicitation pour avoir testé votre code, c'est déjà un très bon reflexe !
Mais faire ça à la main, ca peut vite poser quelques problèmes :
- C'est fatiguant, il faut revérifier à chaque modification !
- On s'y perds
- On risque de rater un détail et de ne pas remarquer une erreur
Donc à la place, vous pouvez utiliser un outils de Python pour faire des tests automatiques : doctest. Voici les tests d'avant écrits en doctest :
# un test est de la forme :
# >>> appel_de_fonction()
# affichage/valeur de retour attendu
def maximum(liste):
"""
>>> maximum([1, 2, 3, 5, 2])
5
>>> maximum([-1, -2, -5, -3])
-1
>>> maximum([-1, 2, 3, -6])
3
"""
res = 0
for e in liste:
if res < e:
res = e
return res
# pour lancer les tests
if __name__ == "__main__":
import doctest
doctest.testmod()
Copiez-collez le code dans un fichier et exécutez-le. Vous devriez voir un affichage comme suit :
**********************************************************************
File "/home/felix/projects/cours_nsi_premiere/test.py", line 9, in __main__.maximum
Failed example:
maximum([-1, -2, -5, -3])
Expected:
-1
Got:
0
**********************************************************************
1 item had failures:
1 of 3 in __main__.maximum
***Test Failed*** 1 failure.
C'est doctest qui vous informe qu'un des tests a échoué :
Vous indique le fichier, la ligne et la fonction dans laquelle le test se trouve.
Indique le test (exemple) qui a raté.
Indique le résultat attendu
indique la valeur que l'appel a réellement retourné
**********************************************************************
1 item had failures:
1 of 3 in __main__.maximum
***Test Failed*** 1 failure.
indique le nombre de tests qui ont raté au total, et au sous-total pour chaque fonction.
Activité : correction
Activité : autre test
Ajoutez un test qui vérifie que la fonction marche quand la liste ne contient qu'une seule valeur.
Executez le fichier et vérifiez que les tests passent.
Appelez votre prof pour confirmer.
On ajoute le test (la valeur n'a pas d'importance):
dans la docstring :
Préconditions : assertions
Voici un code qui calcule, pour chaque élève, la meilleur note qu'il ou elle a eue:
notes = {
"Félix" : [10, 2, 5],
"Julie" : [],
"Mathilde" : [20, 18, 6],
"Matthieu" : [15, 13, 16, 5]
}
def maximum(liste):
"""
>>> maximum([1, 2, 3, 5, 2])
5
>>> maximum([-1, -2, -5, -3])
-1
>>> maximum([-1, 2, 3, -6])
3
>>> maximum([100])
100
"""
res = liste[0]
for e in liste:
if res < e:
res = e
return res
def afficher_meilleure_note(nom, note):
print("Meilleure note de", nom, ":", note)
# pour lancer les tests
if __name__ == "__main__":
import doctest
doctest.testmod()
meilleure_felix = maximum(notes["Félix"])
afficher_meilleure_note("Félix", meilleure_felix)
meilleure_julie = maximum(notes["Julie"])
afficher_meilleure_note("Julie", meilleure_julie)
meilleure_mathilde = maximum(notes["Mathilde"])
afficher_meilleure_note("Mathilde", meilleure_mathilde)
meilleure_matthieu = maximum(notes["Matthieu"])
afficher_meilleure_note("Matthieu", meilleure_matthieu)
Activité : piège
Copiez-collez le code ci-dessus dans un fichier et exécutez-le. Il comporte une erreur.
Sans la corriger, dites quelle ligne est erronnée.
Demandez confirmation à votre prof. Sinon, lisez attentivement la solution.
Vous avez probablement dit la ligne 22... Et vous avez tort.
Mais c'est normal, c'était un piège !
Vous avez lu la trace d'erreur :
Meilleure note de Félix : 10
Traceback (most recent call last):
File "/home/felix/projects/cours_nsi_premiere/test.py", line 41, in <module>
meilleure_julie = maximum(notes["Julie"])
File "/home/felix/projects/cours_nsi_premiere/test.py", line 22, in maximum
res = liste[0]
~~~~~^^^
IndexError: list index out of range
Et l'erreur générée est bien emmise depuis la ligne 22. Mais ce n'est pas cette ligne qui est fautive, c'est la ligne 41.
En fait, la fonction maximum est correcte : elle calcule le maximum d'une liste. Oui, mais d'une liste non vide, parce que le maximum d'une liste vide n'est pas défini.
Or, la ligne 41 appelle maximum sur une liste vide (Julie n'a pas de note). C'est donc la ligne 41 qui est erronée, et non la ligne 22.
On dit que l'appel à la ligne 41 ne respecte pas les préconditions de la fonction maximum.
Toutefois, vous n'aviez pas si tort que ça, parce que ce cas est très ambigu.
Et c'est cette ambiguité qui est le vrai problème : vous auriez peut être perdu du temps à essayer de "corriger" la fonction maximum, alors qu'elle MARCHE DEJA !
Heureusement, Python nous donne des outils pour le palier : les assertions.
Une assertion est une instruction un peu particulière, qui a la forme suivant en Python :
CONDITIONest un booléen, par exemple issu d'une comparaisonMESSAGEest une chaine de caractère, optionnelle.
Si CONDITION vaut True, rien ne se passe.
Si CONDITION vaut False, alors une exception est levée, le programme termine, et l'éventuel message est affiché.
Activité : une assertion
Exécutez les bouts de code suivants dans des fichiers séparés !.
Les assertions sont un outils très utile pour exprimer les préconditions d'une fonction, et aussi pour les vérifier. Dans ce cas là, elle sont placées juste après la ligne def et la docstring.
Voici le code d'avant, avec une assertion ajoutée pour la fonction maximum :
notes = {
"Félix" : [10, 2, 5],
"Julie" : [],
"Mathilde" : [20, 18, 6],
"Matthieu" : [15, 13, 16, 5]
}
def maximum(liste):
"""
>>> maximum([1, 2, 3, 5, 2])
5
>>> maximum([-1, -2, -5, -3])
-1
>>> maximum([-1, 2, 3, -6])
3
>>> maximum([100])
100
"""
###### PRECONDITION AJOUTEE ICI ######
assert len(liste) > 0, "La liste ne doit pas être vide !"
res = liste[0]
for e in liste:
if res < e:
res = e
return res
def afficher_meilleure_note(nom, note):
print("Meilleure note de", nom, ":", note)
if __name__ == "__main__":
import doctest
doctest.testmod()
meilleure_felix = maximum(notes["Félix"])
afficher_meilleure_note("Félix", meilleure_felix)
meilleure_julie = maximum(notes["Julie"])
afficher_meilleure_note("Julie", meilleure_julie)
meilleure_mathilde = maximum(notes["Mathilde"])
afficher_meilleure_note("Mathilde", meilleure_mathilde)
meilleure_matthieu = maximum(notes["Matthieu"])
afficher_meilleure_note("Matthieu", meilleure_matthieu)
Activité
Copiez-collez le code ci-dessus dans un fichier et exécutez-le.
Il est maintenant explicite que l'erreur vient de l'appel et non de la fonction elle-même, et le message qui l'explique est clair.
Activité
Proposez une correction de l'erreur.
Montrez votre solution à votre prof pour confirmation.
Spécifications et type hints
Idéalement, on veut pouvoir comprendre les postconditions (ce qu'elle fait) et les préconditions (à partir de quoi) d'une fonction sans avoir besoin de regarder son code.
Une telle description est appelée spécification. Elle peut inclure :
- Le nom et le nom des paramètres de la fonction.
- La docstring qui décrit ce qu'elle fait.
- Les tests qui donnent des exemples.
- Les assertions.
Par exemple :
def maximum(liste):
"""
Retourne la valeur maximale de liste, une liste d'entiers.
>>> maximum([1, 2, 3])
3
"""
assert len(liste) > 0, "liste ne doit pas être vide"
Parfois, on veut aussi préciser le type des paramètres et des valeurs de retour de la fonction. Il y a trois moyens de le faire :
| Moyen | Paramètres ? | Retour ? | Précis ? | Vérifié par Python |
|---|---|---|---|---|
| docstring | oui | oui | oui | non |
| assertion | oui | non | non | oui |
| types hint | oui | oui | oui | non, mais par d'autres outils |
Docstring
Pour spécifier un type dans la docstring, on l'écrit simplement en français. Comme ci dessus.
Assertions
Pour spécifier le type avec des assertions, on utilise type() qui renvoie le type d'une valeur, et on compare avec is :
Cette méthode a l'avantage d'être vérifiée par Python automatiquement, mais elle pose deux problèmes. Il est assez difficile d'exprimer le type d'une valeur de retour qui soit à la fois simple à lire et vérifiée automatiquement. Il est vraiment compliqué d'exprimer des contraintes précises sur les types, comme une liste d'entiers, de manière efficace.
Activité optionnelle
Cette activité est optionnelle, vous pouvez la passer si vous voulez
Essayez d'exprimer la précondition suivante avec une assertion au lieu d'une docstring :
Pour vérifier que vous avez juste, essayez d'appeler la fonction avec des valeurs correctes et incorrectes, et vérifiez que l'assertion se déclenche quand il faut.
On peut utiliser une fonction intermédiaire:
def est_liste_entiers(liste):
#on vérifie que liste est une liste
if type(liste) is not list:
return False
#On vérifie que chaque élémént de la liste est un entier
for e in liste:
if type(e) is not int:
return False
return True
def maximum(liste):
assert est_liste_entiers(liste), "liste doit être une liste d'entiers".
pass
Type hints
Les versions récentes de Python proposent un outils syntaxique : les type hint, qui permettent d'exprimer les types de manière très précise. Python ignore les type hints et ne les vérifie pas, mais des outils externes comme Pylint peuvent vérifier que les types sont respectés. C'est aussi une syntaxe facile à lire pour les humains.
Pour la précondition vue précédemment, ça se présente comme ça :
plus généralement :
Pour les types composites, comme les listes et les dicts, qui contiennent des valeurs d'autres types, on utilise [] pour spécifier les types contenus. Voici donc les types principaux :
bool # booléen
int # entier
float # flottant
str # chaine
list[T] # liste d'éléments de type T
# par exemple
list[int] # liste d'entiers
dict[K, V] # dictionnaire avec des clés de type K et des valeurs de type V
# par exemple
dict[str, int] # dictionnaire avec des str pour clés et des int pour valeurs
tuple[T1, T2, ...] # tuple avec un premier élément de type T1, un second élément de type T2, etc...
# par exemple
tuple[int, str] # un tuple avec le premier élément de type int, et le second de type str
None # Réprésente l'absence de valeur. Type de retour des fonctions qui ne retournent rien.
Il y a des tas d'autres types. Si vous en voulez quelques-uns en plus, regardez le dépliant suivant (optionel).
Plus de types
n'hésitez pas à poser des questions à votre prof.
from typing import Callable # a mettre en haut du fichier
# fonction qui prends en premier paramètre une valeur de type P1,
# second paramètre une valeur de type P2, etc...
# et retourne une valeur de type R
Callable[[P1, P2, ...], R]
#Par exemple
def egal(a: int, b : int)-> bool:
#...
#a pour type
Callable[[int, int], bool]
T1 | T2 # type union. Représente une valeur qui a pour type soit T1, soit T2
#par exemple
def longueur(chaine_ou_liste : str | list) -> int:
return len(chaine_ou_liste)
from typing import Optional # a mettre en haut du fichiers
Optional[T] # représente une valeur du type T ou None
# équivalent plus clair de
T | None
Literal[V1, V2, ...] # une valeur qui vaut soit V1, soit V2, etc...
# exemple
def deplacer_personnage(directions : Literal["haut", "bas", "droite", "gauche"]) -> None:
#...
# Autre exemple
def calcul_difficile(liste : list[int]) -> int | Literal[False]:
"""
Retourne le résultat si il est trouvé, ou False si il n'est pas trouvé.
"""
Python permet aussi d'exprimer des types génériques, c'est à dire des types incomplets. Par exemple :
# cette fonction est générique sur le type variable T.
# elle prends en paramètre une liste de valeurs de type T
# et une valeur de type T
# et retourne un booléen (True si valeur est dans liste, False sinon)
def contient[T](liste : liste[T], valeur : T) -> bool:
#...
r1 = contient([1, 2, 3], 4) # appel valide, T = int
r2 = contient(["a", "b", "c"], "c") # appel valide, T = str
r3 = contient(["a", "b", "c"], 1) # appel invalide, T n'est pas décidable (int ou str ?)
La syntaxe des hints est très vaste, n'hésitez pas à poser des questions si vous voulez encore creuser !
ATTENTION
Les type hints sont ignorés par Python, mais les anciennes versions comme celles que l'on trouve parfois dans les lycées peuvent générer des erreurs sur certains types composites comme list[int].
Si vous avez une erreur comme :
Vous devrez peut-être mettre des guillements autour de vos hints :
Activité : quizz
Pour chacune des question ci-dessous, donnez la description en français du type hint, ou le type hint correspondant à la description en français.
Un dictionnaire ayant pour clés des entiers, et pour valeurs des listes de chaînes.
Conseils supplémentaires
TDD
On peut écrire les tests d'une fonction avant de les implémenter. C'est un idée que l'on appelle le développement dirigé par les tests (Test Driven Development en anglais).
Le principal avantage est qu'on peut tester la fonction à tout moment pendant son implémentation. Aussi, on est pas tenté de "sauter" les tests une fois qu'on a écrit la fonction.
Le TDD a toutefois quelques limites, si vous voulez en savoir plus, demandez à votre prof.
Testabilité
En rusant un peu, on peut rendre notre code plus facile à tester.
Certaines fonctions sont très faciles à tester, d'autres non.
Activité
Pour chacune des fonctions suivantes :
- si elles sont simples à tester, proposez un ou plusieurs tests en Python.
- Sinon, expliquez pourquoi elles sont compliquées à tester.
def addition(a, b):
"""a et b deux entiers
retourne la somme de a et b
"""
def dessiner_chat(couleur):
"""dessine un chat à l'écran
"""
def nombre_aleatoire(a, b):
"""retourne un nombre aléatoire
compris entre les entiers a et b
"""
def minimum(liste):
"""liste une liste d'entiers non vide
retourne le maximum de liste
"""
Tests unitaires
Je vous