Aller au contenu

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

5
-1
3

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

File "un_chemin_de_fichier.py", line 9, in __main__.maximum

Vous indique le fichier, la ligne et la fonction dans laquelle le test se trouve.

Failed example:
    maximum([-1, -2, -5, -3])

Indique le test (exemple) qui a raté.

Expected:
    -1

Indique le résultat attendu

Got:
    0

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

Corrigez le code de la fonction maximum pour que les trois tests passent.

Quand tous les tests passent, doctest n'affiche rien.

La ligne

res = 0

devient

res = liste[0]

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

>>> maximum([100])
100

dans la docstring :

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 = notes[0]
    for e in liste:
        if res < e:
            res = e
    return res


# pour lancer les tests
if __name__ == "__main__":
    import doctest
    doctest.testmod()

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 :

assert CONDITION

# OU

assert CONDITION, "MESSAGE"
  • CONDITION est un booléen, par exemple issu d'une comparaison
  • MESSAGE est 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 !.

a = 10
assert a == 5, "A doit valoir 5"
a == 3
assert a == 2

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 :

def maximum(liste):
    """
    ...
    """
    assert type(liste) is liste
    #...

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 :

def maximum(liste):
    """
    liste est une liste d'entiers
    """
    pass

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 :

def maximum(liste : list[int]) -> int:
    pass

plus généralement :

def nom_fonction(parametre : TYPE_PARAMETRE, parametre2 : TYPE_PARAMETRE2) -> TYPE_VALEUR_DE_RETOUR

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 :

TypeError: 'type' object is not subscriptable

Vous devrez peut-être mettre des guillements autour de vos hints :

def une_fonction(liste : "list[int]")

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 entier

int
str

une chaîne de caractères

Une liste de chaines de caractères

list[str]
dict[str, bool]

Un dictionnaire ayant pour clés des chaînes et pour valeurs des booléens.

Un Tuple : entier, chaine, booléen

tuple[int, str, bool]

Une liste de listes d'entiers

list[list[int]]
 dict[int, list[str]]

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

TDD