18 - Écrire une fonction

Razik Ikhlef

Cette page complète le cours 18 - Spécification et tests : elle montre comment chercher sur feuille avant de programmer. Les exercices correspondants se trouvent dans la fiche 25 - Spécification et tests .

Quand on doit écrire une fonction, il est tentant de taper du code tout de suite, puis de le modifier au hasard jusqu’à ce que le programme semble fonctionner. On passe alors beaucoup de temps à corriger des erreurs sans avoir compris ce que l’on cherchait.

Il est plus efficace de répondre d’abord, sur une feuille, à deux questions dans cet ordre : que doit faire la fonction, puis comment le fait-elle. L’ordinateur sert ensuite à vérifier une solution déjà trouvée, et non à la chercher.

Les trois zones

La colonne de gauche contient deux zones : en haut, la définition de la fonction, à compléter ; en bas, les appels de la fonction. La colonne de droite forme la troisième zone et sert de brouillon.

Les trois zones (suite)

Feuille de recherche : en haut à gauche la définition de la fonction (paramètres, docstring avec préconditions et postcondition, corps, valeur renvoyée), en bas à gauche les appels et leurs résultats attendus, à droite le brouillon (variables, conditions, boucles, arrêt, structures de données, test à la main, sous-fonction, ajustements)

Les cinq étapes

Les numéros ne suivent pas l’ordre de lecture du programme : le corps de la fonction est écrit en dernier. Les trois premières étapes décrivent ce que fait la fonction (sa spécification) ; les deux dernières, comment elle le fait (son implémentation).

Les cinq étapes (suite)

Les cinq étapes : 1 appels, 2 paramètres, 3 retour (ce que fait la fonction), puis 4 brouillon et 5 corps (comment elle le fait)

Les cinq étapes (suite)

Pourquoi commencer par les appels ? Si l’on n’arrive pas à prévoir à la main le résultat d’un appel, c’est que l’énoncé n’est pas encore compris : il est inutile d’aller plus loin. Ces appels serviront ensuite de tests.

La docstring, contrat de la fonction. Avant toute recherche, on y écrit :

  • les préconditions, c’est-à-dire ce que l’on suppose sur les paramètres ;
  • la postcondition, c’est-à-dire le type du résultat et ce que l’on garantit sur lui.

Les cinq étapes (suite)

La postcondition est placée en tête de la fonction et non près du return, car la docstring est ce que lit la personne qui utilise la fonction (avec help(nom_fonction)), sans regarder le code. On peut aussi suivre la présentation détaillée du cours (sections Paramètres :, Renvoie :, Préconditions :) : seule compte la présence de ces informations.

Les fonctions qui ne renvoient rien. Une fonction qui trie une liste sur place renvoie None. Sa postcondition décrit alors l’état de la liste après l’appel, et c’est cet état que vérifient les tests de la zone 1.

Trouver les cas particuliers

Après le cas classique, on passe en revue les situations suivantes et l’on garde celles qui ont un sens pour la fonction : une structure vide, un seul élément, l’élément en tête, l’élément en fin, un élément absent, des doublons, des valeurs nulles ou négatives, une valeur limite (par exemple une note égale à 10 quand on cherche les notes d’au moins 10).

Le brouillon

Dans la zone 4, on liste ce qui pourrait servir à écrire le corps :

  • les variables et leur valeur initiale ;
  • les conditions et l’ordre dans lequel les tester ;
  • le type de boucle : for si le nombre de tours est connu à l’avance, while s’il dépend d’une condition ;
  • l’organisation des boucles, successives ou imbriquées ;
  • la structure de données adaptée (liste, tuple, dictionnaire) et la meilleure façon de la parcourir, par exemple par valeur ou par indice pour une liste ou un tuple, par clé, par valeur (avec d.values()) ou par couple clé-valeur (avec d.items()) pour un dictionnaire ;
  • un test à la main sur les appels de la zone 1, dans un tableau de suivi des variables.

Le brouillon (suite)

Une boucle while doit s’arrêter : on cherche un variant de boucle, c’est-à-dire une quantité entière positive qui diminue strictement à chaque tour (voir le cours 17 - Correction ). Si le brouillon devient touffu, une partie du travail mérite sans doute sa propre fonction : on la cherche sur une nouvelle feuille, puis on l’utilise comme un outil.

Le brouillon n’a pas à être propre : c’est la seule zone où l’erreur est attendue. Une piste rayée n’est pas du temps perdu, elle montre ce qui ne fonctionne pas.

Premier exemple : compter les occurrences d’un caractère

On cherche une fonction nb_occurrences qui renvoie le nombre de fois où le caractère car apparaît dans la chaîne mot. Le nombre de tours est connu : une boucle for convient.

Premier exemple : compter les occurrences d’un caractère (suite)

Feuille remplie pour nb_occurrences : paramètres car et mot, trois appels (cas classique, caractère absent, chaîne vide), brouillon où la boucle while est rayée au profit d'une boucle for, corps avec un compteur

Second exemple : chercher l’indice d’un élément

On cherche une fonction indice qui renvoie l’indice de la première occurrence de x dans elements, ou -1 si x est absent. Il faut s’arrêter dès que l’élément est trouvé : le nombre de tours n’est pas connu à l’avance.

Second exemple : chercher l’indice d’un élément (suite)

Feuille remplie pour indice : paramètres x et elements, quatre appels (cas classique, élément en tête, élément absent, structure vide), brouillon où la boucle for est rayée au profit d'une boucle while, variant n - i, corps avec un indice i

Second exemple : chercher l’indice d’un élément (suite)

Voici le tableau de suivi pour l’appel indice(9, [2, 5]), qui teste le cas de l’élément absent :

Valeur de i Test i < n Test elements[i] != x Suite
0 True True on continue
1 True True on continue
2 False non évalué on sort

En sortie, i vaut 2, c’est-à-dire n : x est absent et la fonction renvoie -1, comme prévu. Python n’évalue pas elements[2], car la première condition est déjà fausse.

Sur ordinateur

On recopie la fonction avec sa docstring et l’on transforme chaque appel de la zone 1 en assertion. Si une assertion échoue, on lit d’abord le message d’erreur et l’appel concerné : une faute de frappe ou un oubli se corrige directement. En revanche, si l’on ne comprend pas l’erreur ou si l’on modifie le code sans savoir pourquoi, il vaut mieux revenir au brouillon.

Sur ordinateur (suite)

def indice(x, elements):
    """Pré : elements est une liste.
    Post : renvoie un entier, l'indice de la première occurrence
    de x dans elements, ou -1 si x est absent."""
    i = 0
    n = len(elements)
    while i < n and elements[i] != x:
        i = i + 1
    if i == n:
        i = -1
    return i

assert indice(5, [2, 5, 7, 5]) == 1
assert indice(2, [2, 5]) == 0
assert indice(9, [2, 5]) == -1
assert indice(3, []) == -1

Si toutes les assertions sont vérifiées, l’exécution n’affiche rien.

Sur ordinateur (suite)

À éviter À privilégier
Ouvrir l’éditeur dès la lecture de l’énoncé. Écrire d’abord les appels et les résultats attendus.
Oublier les cas particuliers. Passer en revue la liste des cas particuliers.
Corriger au hasard jusqu’à ce que « ça marche ». Dérouler l’algorithme à la main et ajuster le brouillon.
Tester sur un seul exemple, dans la console. Transformer tous les appels en assertions.

S’entraîner

Pour chaque fonction, recopier la feuille (ou imprimer la feuille de recherche ), remplir le brouillon, puis le corps et la valeur renvoyée. Ne passer à l’ordinateur qu’une fois le test à la main réussi.

Somme des chiffres d’un entier

On cherche une fonction somme_chiffres qui renvoie la somme des chiffres d’un entier positif ou nul, par exemple 4 + 0 + 7 + 1 = 12 pour 4 071. On rappelle que n % 10 donne le dernier chiffre de n et que n // 10 le supprime. La spécification est donnée ; il reste à trouver le corps et la valeur renvoyée.

Somme des chiffres d’un entier (suite)

Feuille à compléter pour somme_chiffres : paramètre n, docstring, corps et valeur renvoyée vides, quatre appels (4071 donne 12, 7 donne 7, 100 donne 1, 0 donne 0)

Correction

Feuille remplie pour somme_chiffres : accumulateur s, boucle while n > 0, variant n, corps et valeur renvoyée s

Correction (suite)

Brouillon. On a besoin d’un accumulateur s, initialisé à 0. On retire les chiffres un par un, mais leur nombre n’est pas connu à l’avance (et un entier ne se parcourt pas avec un for) : il faut une boucle while, qui continue tant que n > 0. À chaque tour, on ajoute le dernier chiffre n % 10 à s, puis on le supprime avec n = n // 10.

Arrêt. n est un entier positif qui diminue strictement à chaque tour, car n // 10 < n dès que n > 0 : c’est un variant, la boucle s’arrête.

Correction (suite)

Test à la main sur l’appel somme_chiffres(4071) :

n au début du tour n % 10 s en fin de tour n en fin de tour
4071 1 1 407
407 7 8 40
40 0 8 4
4 4 12 0

Ensuite n > 0 est faux : on sort de la boucle et la fonction renvoie 12, comme prévu. Pour somme_chiffres(0), on n’entre pas dans la boucle et la fonction renvoie 0.

Sur ordinateur.

Correction (suite)

def somme_chiffres(n):
    """Pré : n est un entier positif ou nul.
    Post : renvoie un entier, la somme des chiffres de n."""
    s = 0
    while n > 0:
        s = s + n % 10
        n = n // 10
    return s

assert somme_chiffres(4071) == 12
assert somme_chiffres(7) == 7
assert somme_chiffres(100) == 1
assert somme_chiffres(0) == 0

Nombre d’élèves admis

On cherche une fonction nb_admis qui compte les élèves ayant au moins 10 sur 20 dans un dictionnaire de notes. Il faudra se demander quel parcours du dictionnaire est le plus adapté.

Nombre d’élèves admis (suite)

Feuille à compléter pour nb_admis : paramètre notes, docstring, corps et valeur renvoyée vides, trois appels (cas classique, valeur limite 10, dictionnaire vide)

Correction

Feuille remplie pour nb_admis : compteur, parcours des valeurs avec notes.values(), condition note >= 10, valeur renvoyée compteur

Correction (suite)

Brouillon. On a besoin d’un compteur, initialisé à 0. Seules les notes servent, pas les noms : parcourir les clés (for nom in notes) obligerait à écrire notes[nom] à chaque tour, alors que le parcours des valeurs (for note in notes.values()) donne directement chaque note. Il faut regarder toutes les notes : une boucle for convient, et elle s’arrête après la dernière valeur.

Condition. Un élève est admis si note >= 10. C’est l’appel sur la valeur limite qui permet de choisir entre > et >= : avec note > 10, nb_admis({"Zoé": 10}) renverrait 0 au lieu de 1.

Correction (suite)

Test à la main. Sur l’appel classique, 12 est compté et 8 ne l’est pas : on obtient 1. Sur la valeur limite, 10 est compté : on obtient 1. Sur le dictionnaire vide, on n’entre pas dans la boucle : on obtient 0.

Sur ordinateur.

Correction (suite)

def nb_admis(notes):
    """Pré : notes est un dictionnaire qui associe à chaque nom une note sur 20.
    Post : renvoie un entier, le nombre d'élèves dont la note est
    supérieure ou égale à 10."""
    compteur = 0
    for note in notes.values():
        if note >= 10:
            compteur = compteur + 1
    return compteur

assert nb_admis({"Ana": 12, "Léo": 8}) == 1
assert nb_admis({"Zoé": 10}) == 1
assert nb_admis({}) == 0

Feuille de recherche à imprimer

La feuille de recherche reprend les trois zones, avec un tableau de suivi dans le brouillon. Elle s’imprime en A4 et sert pour n’importe quelle fonction.

Feuille de recherche à imprimer (suite)

Feuille de recherche vierge : définition à compléter, quatre appels (un cas classique et trois cas particuliers), brouillon avec tableau de suivi