Spécification d'une fonction
Introduction
Une fonction n'est pleinement utilisable que si on sait ce qu'elle attend en entrée et ce qu'elle garantit en sortie. Cette description s'appelle la spécification. Elle s'écrit avant le code — c'est ce qui permet à un autre programmeur (ou à vous dans trois semaines) d'utiliser la fonction sans lire son corps.
Une fonction sans spécification, c'est comme une recette de cuisine sans liste d'ingrédients : on peut la lire, on ne sait pas si elle va marcher.
Signature et prototype
La signature (ou prototype) d'une fonction décrit son nom, ses paramètres et le type de chacun. En Python, on l'exprime avec des annotations de type :
def aire_rectangle(longueur: float, largeur: float) -> float:
return longueur * largeur
longueur: floatetlargeur: floatannoncent que les paramètres sont des nombres à virgule.-> floatannonce que la fonction retourne un nombre à virgule.
float est
attendu :aire_rectangle("trois", "quatre") # plante au runtime, pas à cause des annotations
Précondition et postcondition
- Une précondition est une condition que les arguments doivent satisfaire pour que la fonction fonctionne correctement.
- Une postcondition est ce que la fonction garantit en sortie, si la précondition est respectée.
Exemple : une fonction racine_carree(x).
- Précondition :
x >= 0. (On ne sait pas calculer la racine carrée d'un nombre négatif en restant dans les réels.) - Postcondition : le résultat
rsatisfaitr >= 0etr * r == x(à l'erreur d'arrondi près).
C'est un contrat : si vous respectez la précondition, je m'engage sur la postcondition.
La docstring : la spécification en mots
En Python, la spécification d'une fonction s'écrit dans une docstring —
une chaîne de caractères placée juste après la ligne def, encadrée par
trois guillemets """.
def aire_rectangle(longueur: float, largeur: float) -> float:
"""Renvoie l'aire d'un rectangle.
Paramètres :
- longueur : longueur du rectangle, doit être >= 0.
- largeur : largeur du rectangle, doit être >= 0.
Retourne :
- L'aire en unités², toujours >= 0.
Précondition : longueur >= 0 et largeur >= 0.
Postcondition : résultat >= 0.
"""
return longueur * largeur
La docstring est accessible depuis l'extérieur grâce à l'attribut
__doc__ ou à la fonction help() :
Sortie attendue (extrait) :
Renvoie l'aire d'un rectangle de longueur et largeur positives.
Help on function aire_rectangle in module __main__:
aire_rectangle(longueur: float, largeur: float) -> float
Renvoie l'aire d'un rectangle de longueur et largeur positives.
Où se place une docstring dans une fonction Python ?
Garantir une précondition avec assert
Une bonne docstring décrit la précondition. Pour la faire respecter en pratique, on peut ajouter une assertion au début de la fonction.
def racine_carree(x: float) -> float:
"""Renvoie la racine carrée de x.
Précondition : x >= 0.
Postcondition : le résultat r satisfait r >= 0 et r * r ≈ x.
"""
assert x >= 0, "x doit être positif ou nul"
return x ** 0.5
Si la précondition est violée à l'appel, Python lève une AssertionError
explicite, plutôt que de produire un résultat absurde ou cryptique :
print(racine_carree(9)) # 3.0
print(racine_carree(-4)) # AssertionError: x doit être positif ou nul
assert est un outil de défense en développement. En conditions de
production avec l'option python -O, les assertions sont désactivées.
Pour des contrôles indispensables au métier, préférer un if qui lève une
ValueError.Spécification d'une fonction concrète
Reprenons une fonction utile et écrivons sa spécification complète.
def moyenne(notes: list) -> float:
"""Renvoie la moyenne arithmétique d'une liste de notes.
Paramètres :
- notes : liste non vide de nombres (int ou float) entre 0 et 20.
Retourne :
- La moyenne arithmétique, un float entre 0 et 20.
Précondition : notes est non vide ; chaque note est dans [0, 20].
Postcondition : 0 <= résultat <= 20.
Exemples :
>>> moyenne([10, 12, 14])
12.0
>>> moyenne([20, 20])
20.0
"""
assert len(notes) > 0, "La liste de notes ne doit pas être vide"
return sum(notes) / len(notes)
Plusieurs choses à noter :
- La signature annonce types d'entrée et type de retour.
- La docstring liste paramètres, retour, précondition et postcondition.
- Des exemples sous le format
>>> ...aident à comprendre l'usage. - Une assertion garde le contrat.
>>> ... dans une docstring sont la syntaxe
doctest. Python peut les exécuter automatiquement pour vérifier qu'ils
produisent bien le résultat annoncé — une forme légère de test intégré à
la documentation.Bonnes pratiques
- Une fonction, un rôle. Si vous n'arrivez pas à décrire la fonction en une phrase, c'est qu'elle fait sans doute trop de choses.
- Nom parlant :
nb_voyelles_dansest plus utile quecompter. - Préconditions explicites dans la docstring, pas implicites dans le code.
- Types annotés chaque fois que possible.
Quelle est la différence entre précondition et postcondition ?
Pièges courants
- Annotations ne valident pas :
def f(x: int)accepte aussif("bonjour")sans erreur. Penserassertou conditions explicites. - Docstring oubliée : sans elle, votre fonction est une boîte noire pour celui qui veut l'utiliser.
- Précondition trop forte : exiger des conditions inutiles complique
l'usage. Une fonction
est_pair(n)ne devrait pas exigern > 0. - Spécifier après coup : écrire la spécification après le code conduit presque toujours à des fonctions mal pensées. Spécifiez d'abord.
Pour aller plus loin
Une fonction bien spécifiée est aussi une fonction plus facile à tester : les exemples de la docstring sont des cas de test naturels, et la précondition indique quels arguments mettre dans le jeu de tests. C'est l'objet du cours suivant sur la mise au point .