Skip to main content Informatique en prépa - Lycée Malherbe 

2- Qualité du code, tests et preuves

I. Présentation du code

a) Nommer ses variables

b) Opérateurs de calculs

On peut faire des calculs entre des valeurs ou des variables.

a = 3 + 4
b = 13039 % 12

On met toujours des espaces avant et après un opérateur binaire. Pour un opérateur unaire, on ne met pas d'espace.

calcul = 3 * 5
nombre_negatif = -7

c) Les commentaires

Il est important de commenter votre code dès qu'il n'est pas évident ! Par exemple :

a = 3 # assigne 3 à la variable a
cpt = 0 # la variable cpt compte le nombre de bouteilles dans la mer

d) Documenter ses fonctions

On peut (et on doit) ajouter une chaîne de documentation à ses fonctions. On peut ensuite accéder à cette chaîne avec help :

Python

Loading editor...

def salue(personne):
    """
    Prend en argument une personne (chaine de caractères) et la salue.
    Renvoie un booléen, `True` si la salutation a été retournée.
    """
    # code pour saluer une personne

help(salue)

e) Indentation

L'indentation de votre programme doit être parfaitement claire. À l'écrit, on trace des lignes !

II. Programmation défensive

a) Pré-conditions, post-conditions, invariant

Comment faire pour être certain qu'une boucle, ou qu'un bloc d'instructions, est correct ?

On écrit des conditions de la forme suivante :

Complétez les commentaires de la fonction suivante.

def somme(L):
    """
    Cette fonction prend en argument une liste d'entiers, et calcule et renvoie sa somme.
    """
    S = 0
    # pré-condition
    for i in range(len(L)):
        # invariant
        S += L[i]
    # post-condition
    return S
  • pré-condition : L est une liste d'entiers ;
  • invariant : L[i] est un entier et S contient la somme de L[0:i] ;
  • post-condition : S contient la somme de L.

Même question avec les fonctions suivantes.

def insere(L_triee, x):
    """ Cette fonction prend en argument une liste triée d'entiers, et un élément x, et l'insère en place dans la liste. """
    ...

def tri_insertion(L):
    """Cette fonction prend en argument une liste d'entiers, et renvoie une nouvelle liste triée avec l'algorithme du tri par insertion."""
    L_triee = []
    # pré-condition
    for i in range(len(L)):
        # invariant
        insere(L_triee, L[i])
    # post-condition
    return L_triee
  • pré-condition : L_triee est vide, donc triée ;
  • invariant : L_triee est triée, et contient les éléments de L[0:i] ;
  • post-condition : L_triee est triée, et contient les éléments de L.

b) Assertions

Les assertions permettent de tester les conditions et invariants pendant l'exécution du code. Si la condition est fausse, une erreur est levée.

# pré-condition : la liste n'est pas vide
assert len(L) > 0

Par exemple :

Python

Loading editor...

def somme(L):
    """
    Cette fonction prend en argument une liste d'entiers, et calcule et renvoie sa somme.
    """
    S = 0
    #pré-condition : L est une liste d'entiers
    assert type(L) == list
    for i in range(len(L)):
        # invariant : L[i] est un entier et S contient la somme de L[0:i].
        assert type(L[i]) == int
        S += L[i]
    # post-condition : S contient la somme de L.
    return S

print(somme([1, 2, 3]))
print(somme([1, 2.5, 3]))

On peut observer l'invariant avec tracer_variables (du module affichage du site), qui affiche les valeurs des variables à la fin de chaque tour de boucle : à la fin du tour où i vaut , S contient bien la somme de L[0:k+1].

Python
code masqué : pas besoin de le lire pour suivre, exécutez-le simplement

c) Tests

On appelle jeu de tests l'ensemble des tests d'un programme. Comment écrire des tests ?

Par exemple, pour une fonction qui cherche l'indice du maximum d'une liste, on teste : une liste d'un seul élément, un maximum au début, au milieu, à la fin, un maximum présent plusieurs fois, des nombres négatifs.

Python

Loading editor...

def indice_max(L):
    """Renvoie l'indice de la première occurrence du maximum de L (non vide)."""
    i_max = 0
    for i in range(1, len(L)):
        if L[i] > L[i_max]:
            i_max = i
    return i_max

def test_indice_max():
    assert indice_max([7]) == 0                  # un seul élément
    assert indice_max([9, 1, 2]) == 0            # maximum au début
    assert indice_max([1, 9, 2]) == 1            # au milieu
    assert indice_max([1, 2, 9]) == 2            # à la fin
    assert indice_max([3, 9, 1, 9]) == 1         # plusieurs fois : la première occurrence
    assert indice_max([-5, -2, -8]) == 1         # nombres négatifs
    print("Tous les tests sont passés.")

test_indice_max()

Un test qui passe ne prouve pas que la fonction est juste : il montre seulement qu'elle est juste sur cet exemple. "Tester un programme peut montrer la présence de bugs, jamais leur absence" (Dijkstra).

III. Preuves de programme

Lorsqu'on ne veut pas dépendre d'un test, on peut prouver mathématiquement que notre programme est juste.

Définition : terminaison

On dit qu'un programme termine si son exécution ne boucle jamais à l'infini, quelle que soit l'entrée.

Le programme suivant termine-t-il ?

def exp(x, n):
    resultat = 1
    while n != 0:
        resultat *= x
        n -= 1
    return resultat

Pas toujours : si n est négatif, n ne vaut jamais 0 et la boucle ne s'arrête pas. On précise donc que n doit être positif, et on le vérifie avec une assertion :

def exp(x, n):
    """ Prend en entrée un entier x et un entier positif n et renvoie x^n. """
    resultat = 1
    assert n >= 0
    while n != 0:
        resultat *= x
        n -= 1
    return resultat

Pour prouver la terminaison, on utilise un variant de boucle :

Définition : variant de boucle

Un variant de boucle est une quantité entière, positive ou nulle, et strictement décroissante entre chaque tour de boucle.

Dans la fonction exp ci-dessus, n est un variant de boucle : la boucle termine donc.

Définitions : correction partielle, correction totale

  • Un programme est partiellement correct si, lorsqu'il termine, il renvoie le bon résultat (il respecte sa spécification).
  • Il est totalement correct s'il termine et s'il est partiellement correct.

Pour prouver la correction partielle d'une boucle, on utilise un invariant : une propriété

  1. vraie avant la boucle (initialisation) ;
  2. qui, si elle est vraie au début d'un tour de boucle, l'est encore à la fin (conservation) ;
  3. qui, à la sortie de la boucle, donne le résultat voulu (conclusion).

C'est une récurrence sur le nombre de tours de boucle.

Exemple : la fonction exp. Notons et les valeurs des arguments au départ, et la valeur de la variable n au cours de l'exécution. Montrons que la propriété

est un invariant de boucle.

  1. Initialisation : avant la boucle, et , donc .
  2. Conservation : supposons vraie au début d'un tour. Pendant le tour, resultat est multiplié par et diminue de 1 : la nouvelle valeur de est .
  3. Conclusion : à la sortie de la boucle, , donc : la fonction renvoie bien .

Avec la terminaison (le variant ), la fonction est totalement correcte.

On peut observer l'invariant pendant l'exécution :

Python
code masqué : pas besoin de le lire pour suivre, exécutez-le simplement

Exercices

Exercice 1

Proposer un jeu de tests pour une fonction est_triee(L) qui renvoie True si la liste L est triée dans l'ordre croissant.

Liste vide, liste d'un élément, liste triée, liste triée avec des doublons ([1, 2, 2, 3]), liste non triée au début, au milieu, à la fin ([1, 2, 4, 3]), liste triée dans l'ordre décroissant.

Exercice 2

On considère la fonction suivante. Donner un variant pour montrer qu'elle termine, et un invariant pour montrer qu'elle renvoie la somme des éléments de L.

def somme(L):
    S = 0
    i = 0
    while i < len(L):
        S += L[i]
        i += 1
    return S
  • Variant : , entier positif qui diminue de 1 à chaque tour.
  • Invariant : . Il est vrai au départ (, , somme vide) ; s'il est vrai au début d'un tour, après S += L[i] et i += 1, on a avec la nouvelle valeur de . À la sortie, : est la somme de tous les éléments.
Exercice 3

Pour la recherche dichotomique (avec i borne incluse et j borne exclue), donner un variant et un invariant.

  • Variant : , qui diminue strictement à chaque tour (on remplace par , ou par ).
  • Invariant : si est dans , alors il est dans . À la sortie (), est vide : n'est pas dans , et la fonction a raison de renvoyer None.