Les fonctions sont les briques d'un programme structuré

Le jeu plus petit / plus grand

Examinons le texte ci-dessous :

In [ ]:
""" Jeu plus grand / plus petit

    Le programme cherche à deviner un nombre entre 1 et 100
    en posant des questions à un humain qui doit répondre par
    =, < ou > pour dire si le nombre est égal, plus petit ou
    plus grand que le nombre proposé.
"""


def essai(n):
    """ Essaie la valeur n.
        Interroge l'humain jusqu'à ce que celui-ci
        fournisse une réponse valide.
        Retourne 0 si l'humain répond '='
        Retourne -1 si l'humain répond '<'
        Retourne 1 si l'humain répond '>'        
    """
    reponse_valide = False
    while not reponse_valide:
        question = "Le nombre est-il " + str(n) + " ?[=<>]" 
        rep = input(question)
        if rep == '=':
            reponse = 0
            reponse_valide = True
        elif rep == '<':
            reponse = -1
            reponse_valide = True
        elif rep == '>':
            reponse = 1
            reponse_valide = True
    return reponse


def recherche_nombre(a, b):
    """ Interroge l'humain pour trouver le nombre entre a et b.
        Retourne le nombre de questions posées.
    """
    compteur = 0    # Nombre de questions posées
    trouve = False  # Passe à True dès que la réponse est trouvée
    u = a
    v = b
    while not trouve and u < v:
        compteur = compteur + 1
        n = (u + v) // 2
        reponse = essai(n)
        if reponse == -1:
            v = n - 1
        elif reponse == 1:
            u = n + 1
        else:
            trouve = True
    return compteur


input("Choisissez un nombre entre 1 et 100"
      " puis appuyer sur ENTER")
compteur = recherche_nombre(1, 100)
print("Nombre de coups : ", compteur)

Il s'agit d'un programme python complet. Sauvegarder ce texte dans un fichier que vous nommerez par exemple jeu.py. Vous pourrez alors exécuter l'interpréteur Python en lui fournissant ce fichier. Pour cela il vous suffit de taper la commande suivante dans un terminal :

python3 jeu.py

Vous devez observer que Python3 vous demande de choisir un nombre entre 1 et 100 et cherche à deviner ce nombre en vous posant des questions.

Un code source lisible et modulaire

Le programme comporte 58 lignes de code. C'est un programme bien modeste comparé à ceux des logiciels que vous utilisez quotidiennement. Pourtant, déjà ici, le soucis de produire du code de qualité est payant :

  • Un code plus facile à lire et à comprendre est plus facile à corriger et à faire évoluer. Comment savoir si le code est correct quand celui-ci est illisible ?
  • Un code modulaire est plus simple à comprendre. Chaque partie peut être comprise et corrigée de façon indépendante. De plus, les modules peuvent être adaptés et réutilisés dans d'autres contextes. Les informaticiens ne réinventent pas la roue sans arrêt.

Il est très difficile pour un humain de comprendre une centaine de lignes de code étroitement enchevêtrées les unes dans les autres. Pour cette raison un code bien conçu est structuré en une collection de fonctions dont chacune dépasse rarement une quinzaine de lignes de code. L'art du programmeur est de choisir un découpage de telle sorte que les interfaces entre les fonctions soient réduites et où chacune d'elles accomplit une tâche simple et clairement identifiée. Les fonctions sont des briques. Le programmeur fabrique les fonctions et les assemblent pour réaliser un programme.

Le code source du programme jeu.py

Le code source comporte :

  • La doctring du programme. C'est une chaîne de caractères qui décrit ce que fait le programme, c'est-à-dire son mode d'emploi.

  • La définition des fonctions. Il s'agit ici des fonctions essai et recherche_nombre.

  • Le programme principal.

Le programme principal se réduit à 4 lignes :

  • Lignes 55-56 : Affichage d'une consigne à l'humain et attente de la touche ENTER.
  • Ligne 57 : Recherche du nombre entre 1 et 100.
  • Ligne 58 : Affichage du nombre de questions posées.

Ces 4 lignes sont simples à comprendre. Le travail principal qui consiste à rechercher le nombre se résume à l'instruction recherche_nombre(1, 100). La façon dont on recherche effectivement ce nombre est détaillée ailleurs dans le code : lignes 34-52.

Le code de la fonction recherche_nombre fait lui-même appel à une sous-fonction essai (ligne 45). Ceci permet de conserver une taille raisonnable pour la fonction recherche_nombre en déléguant à la fonction essai le travail consistant à récupérer une réponse valide de la part de l'humain.

La définition d'une fonction

Observons les lignes 34-52 qui définissent la fonction python recherche_nombre :

  • Ligne 42 : La signature ou en-tête de la fonction. On peut y lire le nom de la fonction recherche_nombre ainsi que les noms de ses arguments a et b. La fonction ne décrit pas comment rechercher un nombre spécifiquement entre 1 et 100, mais plus généralement entre a et b. Ceci n'est pas plus difficile et permet d'obtenir un code plus lisible et plus général. Il suffit de changer la ligne 57 en recherche_nombre(1, 500) pour obtenir un programme qui recherche un nombre entre 1 et 500.

  • Ligne 35-37: La doctring de la fonction. C'est une chaîne de caractères qui donne la spécification externe de la fonction, c'est-à-dire la description de ce que fait exactement cette fonction. C'est le contrat entre le programmeur utilisateur de la fonction et le programmeur du code de la fonction.

  • Ligne 38-52 : Le code de la fonction. Ce sont les instructions python qui produisent l'effet décrit dans la doctring. Ce code doit être écrit avec soin. Il doit répondre à deux exigences aussi importantes l'une que l'autre :

    • Le code doit être correct : il doit produire les bons effets lorsqu'il est lu par l'interpréteur Python.
    • Le code être facile à lire et à comprendre pour un humain.

Remarque :

Une docstring n'est pas un commentaire. Elle est lue par l'interpréteur Python. La fonction help renvoie la doctring d'une fonction :

In [ ]:
help(recherche_nombre)
Help on function recherche_nombre in module __main__:

recherche_nombre(a, b)
    Interroge l'humain pour trouver le nombre entre a et b.
    Retourne le nombre de questions posées.

Dans un notebook Jupyter lorsque le curseur est sur le nom d'une fonction, la séquence de touches Shift-TAB affiche la doctring de cette fonction.

Communication entre les fonctions

Lorqu'une fonction A (ou le programme principal) délègue une tâche à une fonction B. La fonction appelante A fournit des informations à la fonction appelée B par l'intermédiaire des arguments de B. Par exemple, ligne 57, le programme principal fournit à recherche_nombre les bornes de recherche a et b.

Lorque la fonction appelée B termine son travail, elle renvoie une information à la fonction appelante A. L'information renvoyée est la valeur de l'expression à droite du mot-clé return. Ce mot-clé return met ainsi immédiatement fin à l'exécution de la fonction B et la fonction appelante A reprend alors son exécution.

Concevoir une fonction

Exemple : calcul de la racine carrée

Nous nous proposons d'écrire un code Python qui calcule la racine carrée d'un nombre $x$. Un tel code a une utilité générale évidente. Il ne s'agit donc pas d'écrire un programme complet qui interroge un humain pour connaître le nombre $x$ et qui imprime ensuite la valeur de la racine carrée de ce $x$. Il s'agit plutôt d'écrire une fonction sqrt qui pourra être utilisée par n'importe quel autre code. Il ne nous importe pas de savoir ce que cet autre code fera de la réponse. Il est, entre parenthèses, plus problable que cette réponse soit utilisée dans un calcul plus complexe plutôt que d'être affichée sur un quelconque écran à destination d'un humain.

Voici la signature et la docstring de la fonction :

In [ ]:
def sqrt(x):
    """ Retourne la racine carrée de x"""

Il reste à écrire le code de la fonction. Pour cela il nous faut trouver un algorithme : comment calculer la racine carrée d'un nombre x.

Une idée consiste à considérer un rectangle quelconque de côtés $a$ et $b$ tels que l'aire soit égale à $x$. En remplaçant $a$ par la moyenne de $a$ et de $b$ et en ajustant $b$ pour conserver une aire égale à $x$ on obtient un nouveau rectangle dont la forme se rapproche de celle d'un carré. En itérant suffisamment de fois le processus on obtient des côtés qui ont une valeur aussi proche que l'on souhaite de $\sqrt{x}$.

On peut formaliser cette idée en definissant la suite $(a_n)_{n\in\mathbf{N}}$ ci-dessous et en montrant qu'elle converge vers $x$.

$$a_0 = x\quad\text{et}\quad a_{n+1} = \frac{1}{2}\left(a_n + \frac{x}{a_n}\right)$$

Le code python ci-dessous calcule les termes $a_n$ jusqu'à obtenir un terme tel que $\left\vert a_n^2-x\right\vert\leq 10^{-14}$.

In [ ]:
def sqrt(x):
    """ Retourne la racine carrée de x"""
    a = x
    while abs(a * a - x) > 1e-14:
        a = (a + x / a) / 2
    return a

Maitenant que cette fonction est définie, on peut l'utiliser pour calculer la racine carrée d'un nombre quelconque et utiliser le résultat comme bon nous semble.

In [ ]:
z = 3 * sqrt(21) - 1

Les modules python

Lorque l'interpréteur Python est installé sur un ordinateur, il est accompagné d'une collection de modules. Il s'agit de bibliothèques de fonctions générales que l'on peut utiliser à notre convenance plutôt que de réinventer la roue. On se doute que la fonction sqrt que nous venons d'écrire existe déjà. Elle est disponible dans le module math.

Pour utiliser les fonctions du module math, il faut importer le module :

In [ ]:
import math

On peut ensuite utiliser les fonctions de ce module :

In [ ]:
e = math.exp(1)
math.sqrt(e)
Out[ ]:
1.6487212707001282

Pour éviter d'écrire le préfixe math on peut utiliser la syntaxe suivante :

In [ ]:
from math import sin, cos

sin(e) ** 2 + cos(e) ** 2
Out[ ]:
1.0000000000000002