Prérequis : fonctions en Python , boucles et variant de boucle vus en première
À l’issue de ce chapitre, vous saurez :
- prévoir à la main les appels d’une fonction et leurs résultats, cas particuliers compris ;
- écrire la spécification d’une fonction (préconditions et postcondition) avant son code ;
- chercher le corps d’une fonction au brouillon et le tester à la main avec un tableau de suivi ;
- transformer les appels prévus en assertions sur ordinateur.
Pourquoi chercher sur feuille ?
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 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).
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.
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 écrire une docstring plus détaillée, comme dans les rappels sur les fonctions
: seule compte la présence de ces informations.
Les fonctions qui ne renvoient rien. Une fonction qui trie une liste sur place ou qui empile un élément renvoie None. Sa postcondition décrit alors l’état de la structure 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 :
forsi le nombre de tours est connu à l’avance,whiles’il dépend d’une condition ; - l’organisation des boucles, successives ou imbriquées ;
- la structure de données adaptée (liste, tuple, dictionnaire, puis pile, file, arbre ou graphe au fil de l’année) 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 (avecd.items()) pour un dictionnaire ; - un test à la main sur les appels de la zone 1, dans un tableau de suivi des variables.
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 (notion vue en première). 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.
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.
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.
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.
| À é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.
Correction
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.
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.
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é.
Correction
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.
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.
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.
Pour continuer