Aller au contenu

11 Cours Prototype Documentation

1 Introduction⚓︎

On récupère ici le fichier bibliothèque.py et dans un fichier essai.py on importe la fonction Mafonction, puis on lance quelques tests :

🐍 Script Python
from bibliothèque import Mafonction

print(Mafonction(5))
donne :
📋 Texte
0.4472135954999579

🐍 Script Python
print(Mafonction(0))
donne :
📋 Texte
Traceback (most recent call last):
  File "H:/Boulot/2023-2024/1ere-NSI/essaiBibli.py", line 2, in <module>
    print(Mafonction(0))
  File "H:/Boulot/2023-2024/1ere-NSI\bibliothèque.py", line 4, in Mafonction
    return 1 / sqrt(x)
ZeroDivisionError: float division by zero

🐍 Script Python
print(Mafonction(-5.2))
donne :
📋 Texte
Traceback (most recent call last):
  File "H:/Boulot/2023-2024/1ere-NSI/essaiBibli.py", line 2, in <module>
    print(Mafonction(-5.2))
  File "H:/Boulot/2023-2024/1ere-NSI\bibliothèque.py", line 4, in Mafonction
    return 1 / sqrt(x)
ValueError: math domain error

Ces erreurs inattendues, qui résultent du fait que l’on ne sait pas ce que fait la fonction, ne sont pas souhaitables. C’est pourquoi il faut, au minimum, documenter sa fonction :

🐍 Script Python
from math import sqrt
def Mafonction(x):
    """ Mafonction renvoie l'inverse de la racine carrée du nombre choisi """
    return 1 / sqrt(x)

La docstring apparaît lorsque l’on saisit le nom de la fonction en console. Mais il y a beaucoup mieux!

2 Prototype de fonction, documentation⚓︎

Prototype de fonction⚓︎

Quand on veut utiliser une fonction, pour l’appeler, on a besoin des informations suivantes :

  • son nom ;
  • ses paramètres et leurs types ;
  • le type de la valeur de retour (s’il y en a une).

Ces informations sont communément appelées le prototype de la fonction. Cela correspond en fait, dans le code source, à l’en-tête de la fonction, qui explique le fonctionnement de la fonction, l’idée étant de pouvoir l’utiliser sans savoir comment cette fonction est codée.
Par exemple, plusieurs présentations possibles du prototype de la fonction :

  • en "docstring", entre 3 guillemets, juste après l'en-tête de la fonction (méthode à privilégier) :

    🐍 Script Python
    def Mafonction(x):
        """ Mafonction renvoie l'inverse de la racine carrée du nombre choisi
        Entrée : x (float)
        Sortie : float
        """
        return 1 / sqrt(x)
    

  • en commentaire juste avant l'en-tête de la fonction :

    🐍 Script Python
    # Mafonction : renvoie l'inverse de la racine carrée du nombre choisi
    # Entrée : x (float)
    # Sortie : float
    def Mafonction(x):
        return 1 / sqrt(x)
    

  • on peut aussi juste annoter les paramètres et le retour avec les types appropriés. Cela donne une vision synthétique des types, mais ne donne pas de précisions sur les différents rôles. Python ne vérifie pas que l'utilisateur fournit les types demandés.

🐍 Script Python
def Mafonction(x : float) -> float:
    return 1 / sqrt(x)

Spécification : précondition, postcondition⚓︎

On peut aller plus loin dans le prototype de la fonction. Pourquoi? Parce qu’un programme peu ou mal documenté est difficile à corriger, modifier et réutiliser. Il est donc important de documenter et de commenter systématiquement son code.

Pour chaque fonction, on rédige dans la documentation : - le rôle de la fonction ; - une ligne pour chaque paramètre d’entrée, indiquant son type, ce que ce paramètre représente et éventuellement une propriété attendue par la fonction sur l’entrée, que l’on appelle précondition ; - une ligne pour la valeur renvoyée, son type, ce qu’elle représente et éventuellement une propriété promise par la fonction sur cette sortie, que l’on appelle postcondition.

En reprenant notre exemple :

🐍 Script Python
def Mafonction(x):
    """ Mafonction renvoie l'inverse de la racine carrée du nombre choisi
    Entrée : paramètre x (float)
    Sortie : float
    """
    return 1 / sqrt(x)

La spécification des fonctions ainsi fournie doit permettre de les utiliser sans avoir à connaître leur code.

Protéger son programme : vérifier la précondition⚓︎

Exemple :
La fonction suivante calcule la racine carrée du double d'un nombre flottant.

🐍 Script Python
from math import sqrt

def racine_du_double(x):
    return sqrt(2*x)
Quelle est la précondition sur les arguments de cette fonction ?

  • \(x >= 0\)
  • \(2 \times x > 0\)
  • \(x < 0\)
  • \(\sqrt{x} >= 0\)

On peut rédiger la documentation, et laisser l'utilisateur s'assurer qu'il fait un bon usage de la fonction.
Ou alors, on peut "protéger" sa fonction pour garantir les préconditions sur les arguments de la fonction.
Pour cela, on peut utiliser des assertions (mot clé assert) qui signifie en substance "assure-toi que la condition est vraie, sinon, arrête la fonction en envoie un message erreur adapté".

Par exemple, avec notre exemple précédent Mafonction :

🐍 Script Python
from math import sqrt
def Mafonction(x):
    """ Mafonction renvoie l'inverse de la racine carrée du nombre choisi
    """
    assert x > 0, "Impossible, le nombre est négatif, précondition non remplie !"
    return 1 / sqrt(x)

🐍 Script Python
print(Mafonction(0))
ou
🐍 Script Python
print(Mafonction(-5.2))
donnent :
📋 Texte
Traceback (most recent call last):
  File "H:/Boulot/2023-2024/1ere-NSI/essaiBibli.py", line 2, in <module>
    print(Mafonction(-5.2))
  File "H:/Boulot/2023-2024/1ere-NSI\bibliothèque.py", line 5, in Mafonction
    assert x > 0, "Impossible, le nombre est négatif, précondition non remplie !"
AssertionError: Impossible, le nombre est négatif, précondition non remplie !

Programmation défensive ou gestion des erreurs

Programmer avec des préconditions utilisant assert est ce qu'on appelle de la "programmation défensive". Elle garantit les préconditions, et donc l'utilisation des fonctions comme elles étaient prévues par le développeur.
En revanche, cela signifie aussi que toute utilisation qui dévierait des préconditions va arrêter tout le programme.
Lorsque l'on programme à destination d'autres personnes, qui ont davantage de risque de mal utiliser les fonctions, afin de ne pas tout bloquer, on fera davantage de la gestion active d'erreur (avec des if ... else des try ... except) en prévoyant des valeurs de retour spéciales.

3 Tester son programme⚓︎

Généralités⚓︎

Edsger Dijkstra (1930-2002) est un mathématicien et informaticien néerlandais du xxe siècle. Il reçoit en 1972 le prix Turing.
Une citation : « Le test de programmes peut être une façon très efficace de montrer la présence de bugs mais est désespérément inadéquat pour prouver leur absence ».
Ne pas perdre de vue qu’un programme peut passer une infinité de tests sans pour autant fournir le résultat attendu. Imaginons une fonction retournant le double d’un nombre entier s’il est pair et son triple s’il est impair. On peut réaliser avec succès une infinité de tests visant à vérifier que la fonction retourne le double du nombre entier saisi sans que cela soit correct pour tout entier, en testant uniquement des entiers pairs.
Nous verrons plus tard des méthodes (mathématiques !) autres que les tests, permettant de prouver qu’un algorithme retourne la bonne valeur, et pour prouver ces choses-là, la spécification aura toute son importance (si l’on ne précise pas ce qu’est censé faire l’algorithme, on ne pourra pas prouver qu’il le fait !).

Néanmoins, tester son programme est important. Et il est important de prévoir un panel de tests variés, qui couvrent autant que possible les différents cas possibles rencontrés lors de l'exécution de la fonction.

On peut, par exemple, essayer de couvrir des cas entiers, des cas flottants, les cas "limites", comme zéro, lorsque c'est possible, etc.

Question :
On écrit une fonction qui prend en paramètre une liste non vide et qui renvoie son plus grand élément. Combien de tests faudrait-il écrire pour garantir que la fonction donne un résultat correct pour toute liste ?

  • Deux tests : pour une liste à un élément et pour une liste à deux éléments ou plus.
  • Deux tests : pour le cas où le plus élément est en début de liste, et pour le cas où le plus grand élément n'est pas en début de liste.
  • Trois tests : pour une liste vide, pour une liste à un élément, et pour une liste à deux éléments ou plus.
  • Il faudrait écrire une infinité de tests : on ne peut pas prouver que cette fonction est correcte, simplement en la testant.

Comment automatiser les tests ?⚓︎

Jusqu'à maintenant, tester une fonction signifiait appeler la fonction avec certaines valeurs, faire afficher le résultat, et comparer nous-même le résultat affiché avec le résultat que l'on attendait.
Mais il possible d'automatiser les tests, et laisser l'ordinateur vérifier si le résultat de la fonction est celui que l'on attend.

Tester son programme avec assert⚓︎

Des assertions (mot clé assert) peuvent permettre de tester des postconditions sur les retours de la fonction, ou même pour tester des valeurs retournées connues d'avance.

Par exemple, avec notre exemple précédent Mafonction :

🐍 Script Python
from math import sqrt
def Mafonction(x):
    """ Mafonction renvoie l'inverse de la racine carrée du nombre choisi
    """
    assert x > 0, "Impossible, le nombre est négatif, précondition non remplie !"
    return 1 / sqrt(x)
🐍 Script Python
assert Mafonction(4) > 0 # garantir une postcondition
assert Mafonction(4) == 0.5 # tester une valeur de retour
# mais attention aux égalités de flottants !!

Lorsque les tests sont réussis, aucun message n'est envoyé, rien ne s'affiche. Mais si un test échoue, tout s'arrête, et cela produit une erreur.
Inconvénient : dès qu'un test échoue, tout s'arrête. Cela ne permet pas de lancer plusieurs tests en même temps.

Tester son programme avec une bibliothèque dédiée⚓︎

Il existe des bibliothèques de tests dédiées à Python, notamment doctest et pytest. Tester ce code (éventuellement avec pyzo en faisant "pip install doctest" dans la console si cela ne fonctionne pas avec IDLE) :

🐍 Script Python
import doctest
def somme(maliste):
    """
    Fonction qui calcule la somme des nombres d'une liste non vide.
    Paramètre : maliste (list) une liste d'entiers non vide
    Valeur renvoyée : la somme des éléments de la liste, de type int
    Exemples :
    >>> somme([3])
    3
    >>> somme([1, 2, 3, 4, 5])
    15
    >>> somme([12, -17, 3, -2, 11, -13, 6])
    0
    """
    s = 0
    for nombre in maliste:
        s = s + nombre
    return s

help(somme)
print(doctest.testmod())

Syntaxe : les tests à effectuer doivent être indiqués dans le Docstring, ligne par ligne, avec l'appel de fonction derrière les 3 chevrons >>>, et avec le résultat attendu sur la ligne suivante.
On peut en préciser autant que souhaité.

Remarque : comme les tests sont indiqués dans le Docstring, ils apparaîtront pour l'utilisateur qui demande à afficher help.
C'est un avantage, car cela donne des exemples à l'utilisateur, mais cela limite le nombre de tests que l'on peut raisonnablement faire sans alourdir la documentation.