cours1 min de lecture

Spécification d'une fonction

Signature, précondition, postcondition et docstring — décrire ce qu'une fonction promet avant de l'écrire.
programme

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: float et largeur: float annoncent que les paramètres sont des nombres à virgule.
  • -> float annonce que la fonction retourne un nombre à virgule.
Les annotations ne sont pas contraignantes en Python. Le code suivant ne lèvera aucune erreur même si on passe une chaîne là où un float est attendu :
aire_rectangle("trois", "quatre")   # plante au runtime, pas à cause des annotations
Elles servent à documenter et à aider les outils d'analyse (linters, éditeurs). C'est une convention de lisibilité, pas une vérification.

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 r satisfait r >= 0 et r * 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() :

⏵ Ctrl+↵ pour exécuter
Aucune exécution pour l'instant.

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 :

  1. La signature annonce types d'entrée et type de retour.
  2. La docstring liste paramètres, retour, précondition et postcondition.
  3. Des exemples sous le format >>> ... aident à comprendre l'usage.
  4. Une assertion garde le contrat.
Les exemples au format >>> ... 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_dans est plus utile que compter.
  • 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 aussi f("bonjour") sans erreur. Penser assert ou 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 exiger n > 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 .