UKOnline

Découpe en sous-problèmes

À quoi servent les fonctions en pratique ? Comme on a pu le constater, elles permettent de réutiliser du code et évitent ainsi de la duplication, tout en rendant le programme plus lisible. Les fonctions permettent également de structurer un programme, fournissant ainsi une documentation implicite de ce dernier.

Lorsque vous devez écrire du code très similaire à du code déjà écrit, on pourrait sans doute croire que la meilleure solution consiste à copier-coller un bout de code, puis de l'adapter. Il s'agit pourtant d'une très mauvaise pratique, pour deux raisons principales :

  • les chances de se tromper à un moment lors des copier-coller ou des adaptations de ces derniers sont grandes ;
  • si une erreur est détectée dans le code qui a été copié-collé, il va falloir la corriger partout là où ce code a été copié-collé.

Décomposition

Le second intérêt des fonctions est de structurer un programme. Grâce à ces dernières, on va pouvoir découper un gros programme en petits blocs, chacun plus simple à comprendre. Voyons cela avec l'exemple suivant qui permet d'afficher les $n$ premiers nombres premiers (pour rappel, un nombre naturel $n$ est premier s'il admet exactement deux diviseurs distincts (qui seront dès lors $1$ et $n$)) :

Comprendre ce programme sans être celui qui l'a écrit n'est pas du tout facile. Tout d'abord, il n'y a aucun commentaire permettant d'aider le lecteur et le nom de toutes les variables n'est pas forcément explicite. Ensuite, ce code contient deux boucles imbriquées, ce qui rend sa compréhension moins aisée, de prime abord.

Afin de rendre le programme plus clair, il faut définir et utiliser des fonctions. Pour cela, on va décomposer le problème qu'on nous demande de résoudre en sous-problèmes. On peut en identifier trois :

  • tester si un nombre est un diviseur d'un autre ;
  • tester si un nombre est premier ;
  • afficher les $n$ premiers nombres premiers.

Une fois ces sous-problèmes résolus, on pourra les combiner pour résoudre le problème principal. Commençons par définir une fonction isDivisor permettant de tester si un nombre est un diviseur d'un autre :

Pour tester si le nombre d est un diviseur de n, il suffit de vérifier si le reste de la division entière de n par d est nul ou non. Le corps de la fonction est simplement composé d'une instruction qui renvoie un booléen (True si d est un diviseur de n et False sinon).

Il nous faut ensuite une fonction isPrime qui permet de tester si un nombre n est un nombre premier ou non :

La fonction compte le nombre de diviseurs que possède le nombre n. Pour cela, elle parcourt tous les nombres compris entre $1$ et $n$, et teste, grâce à la fonction isDivisor précédemment définie, s'ils divisent ou non n. On termine en vérifiant que le nombre de diviseurs distincts doit être de deux, en renvoyant donc un booléen (True si n est un nombre premier et False sinon).

Enfin, il nous reste maintenant à définir une fonction printPrimes qui permet d'afficher des nombres premiers. Pour cela, on va parcourir tous les nombres naturels l'un après l'autre, et afficher les nombres premiers, jusqu'à en avoir affiché suffisamment. Le listing de la figure 2 montre le programme final, où l'on peut voir la fonction printPrimes qui utilise la fonction isPrime précédemment définie.

Le fichier prime-numbers.py contient un programme permettant d'afficher une séquence de nombres premiers.

L'exécution du programme présenté au listing de la figure 2 affiche donc la séquence des sept premiers nombres premiers :

2
3
5
7
11
13
17

Découper un problème en sous-problèmes, et par conséquent définir plusieurs fonctions de plus petites tailles, permet de rendre un programme plus lisible. Il s'agit de manière générale d'un bon réflexe à suivre. Il ne faut pas avoir peur de définir plusieurs petites fonctions, même si leur corps ne fait qu'une instruction.

De plus, les différentes fonctions définies pourraient être réutilisées dans d'autres programmes ultérieurs. En procédant de la sorte, on se constitue un stock de fonctions à utiliser pour en définir de nouvelles.

Spécification

Lorsqu'on définit des fonctions, c'est évidemment dans le but de les utiliser. Il est donc très important de les documenter, c'est-à-dire d'ajouter des commentaires expliquant ce qu'elles font, et comment les utiliser. On pourrait par exemple écrire :

Ce commentaire qu'on a ajouté décrit ce que fait la fonction, ainsi que les paramètres qu'il faut lui fournir. Il est donc complet et permet d'utiliser la fonction comme il faut.

On peut décrire une fonction de manière plus systématique en fournissant sa spécification. Pour cela, on doit décrire deux choses :

  • les préconditions d'une fonction sont toutes les conditions qui doivent être satisfaites avant de pouvoir appeler la fonction, que ce soit sur des variables globales ou sur ses paramètres ;
  • les postconditions d'une fonction sont toutes les conditions qui seront satisfaites après appel de la fonction, si les préconditions étaient satisfaites, que ce soit sur des variables globales ou sur l'éventuelle valeur renvoyée.

Voyons tout de suite comment spécifier la fonction isDivisor en définissant ses préconditions et postconditions :

Pour appeler la fonction, il faut donc lui fournir deux nombres entiers positifs en paramètres, et s'assurer que d soit différent de zéro. Dans ce cas, après avoir appelé la fonction, la valeur qu'elle aura renvoyé contiendra True si d est un diviseur de n et False sinon.

La spécification d'une fonction contient donc toute la documentation nécessaire pour l'utiliser correctement. S'il ne faut pas devoir lire le corps de la fonction pour comprendre ce qu'elle fait et comment l'utiliser, c'est que la spécification est bien écrite.