Enchaînements d’instructions

Dans ce chapitre, nous abordons l’écriture d’un algorithme à proprement parler. Après avoir présenté sa structure générale, nous présenterons les différents enchaînements d’instructions qui permettent de le définir.

Fonction

En Python, le code décrivant un algorithme s’appelle une fonction. La structure générale d’une fonction est décrite ci-dessous. Notons que toutes les lignes à l’exception de la première doivent commencer par quatre espaces [1].

  • première ligne :
    • le mot-clé def,
    • le nom de la fonction,
    • la liste, entre parenthèses, des noms des paramètres d’entrée, séparés par des virgules,
    • le caractère deux points (:).
  • rappel de la spécification du problème :
    • précédée et suivie par des triples guillemets(""").
  • enchaînement d’instructions décrivant l’algorithme à proprement parler :
    • décrit plus en détail dans la suite de ce chapitre.
  • dernière ligne :
    • le mot-clé return [2],
    • la liste des noms des paramètres de sortie, séparés par des virgules.

Par exemple, la fonction encadrant la racine carrée d’un nombre réel x aura la structure suivante :

def encadre_racine_carrée(n):
    """
    :entrée x:    réel
    :pré-cond:    x ≥ 0
    :sortie inf:  entier
    :sortie sup:  entier
    :post-cond:   inf ≤ √x < sup
    :post-cond:   |sup - inf| ≤ 1
    """
    # (...) enchaînement d'instructions
    return inf, sup

La fonction calculant le nombre de bonbons restant à Alice aura la structure suivante :

def bonbons_restants(n, p):
    """
    :entrée n:  entier
    :entrée p:  entier
    :pré-cond:  0 ≤ p ≤ n
    :sortie r:  entier
    :post-cond: r + p = n
    """
    # (...) enchaînement d'instructions
    return r

Rappelons que le rappel de la spécification ne joue aucun rôle pour l’ordinateur. Il n’est là que pour documenter la fonction, en rappelant au programmeur le problème qu’elle doit résoudre. Pour cette raison, il est donc recommandé de ne pas négliger cet aspect, même s’il n’a pas d’incidence directe sur l’exécution de la fonction par l’ordinateur.

Procédure

Une procédure est une fonction qui n’a pas d’instruction return, où dont l’instruction return n’est suivie d’aucune variable. Cela peut sembler contradictoire avec la manière dont nous avons défini la spécification d’un problème et l’écriture d’une fonction, et mérite donc une explication.

La première utilisation des procédures concerne les problèmes dont la « solution » est en dehors de la mémoire de l’ordinateur. Un exemple typique est d’afficher un texte à l’écran. Le problème est décrit par un paramètre d’entrée (une chaîne de caractères). En revanche la solution ne peut pas être décrite par une valeur (ou un ensemble de valeurs), mais par un effet sur le monde extérieur. Notons que, pour cet exemple précis, Python fournit la procédure print.

La deuxième utilisation des procédures concerne les fonction n’ayant aucun paramètre de sortie strict, mais des paramètres d’entrée-sortie (cette notion sera vue au chapitre Tableaux).

Variables au sein d’une fonction

On a présenté au chapitre précédent la notion de variable. Il convient de décrire ici les différentes catégories de variables utilisées dans une fonction. Elles sont au nombre de trois :

  • les variables correspondant aux paramètres d’entrée ;
  • les variables correspondant aux paramètres de sortie ;
  • les variables intermédiaires.

Les variables correspondant aux paramètres d’entrée ont une particularité : elles ont déjà une valeur au début de la fonction (on verra dans la section Appel de fonction d’où vient cette valeur). Elles n’ont donc pas besoin d’être affectées, et on évitera en général de changer leur valeur [3].

Les variables correspondant aux paramètres de sortie, quant à elles, doivent absolument être affectées dans la fonction, puisque c’est le rôle de cette dernière de déterminer leur valeur (rappelons que les paramètres de sortie décrivent la solution au problème que l’on cherche à résoudre). Ces valeurs sont transmises à l’appelant par l’instruction return à la fin de la fonction.

Les variables intermédiaires, enfin, sont toutes les autres variables qui peuvent être nécessaires au calcul des paramètres de sortie. Par définition, elles n’ont pas de valeur initialement (elle doivent donc être affectées avant d’être utilisées), et leur valeur est « oubliée » à la fin de la fonction (puisqu’elles ne sont pas données à l’instruction return).

Si l’on voit la fonction comme une boîte noire dont le rôle est d’effectuer une opération précise, alors :

  • les paramètres d’entrée contiennent les valeurs passées à la boîte noire ;
  • les paramètres de sortie contiennent les valeurs retournées par la boîte noire ;
  • les variables intermédiaires sont invisibles en dehors de la boîte noire.

Imaginons que le rôle de notre boîte noire soit de déterminer quelles sont la plus petite et la plus grande valeur d’une liste de valeurs, ainsi que la moyenne des éléments de la liste. La liste de valeur est notre paramètre d’entrée, le minimum, le maximum et la moyenne sont les paramètres de sortie, et on imagine aisément que d’autres variables temporaires sont utilisées à l’intérieur de la fonction pour effectuer les calculs intermédiaires (par exemple, la somme des valeurs et le nombre de valeurs, nécessaires au calcul de la moyenne).

Séquence

La forme la plus simple d’enchaînement d’instructions composant une fonction est la séquence. Les instructions sont écrites l’une après l’autre, séparées par un saut de ligne. Pour Python, il est important qu’elles soient toutes au même niveau d’indentation (c’est-à-dire précédées du même nombre d’espaces). Plus généralement, l’indentation est un aspect important de l’écriture d’algorithmes. En effet, un algorithme bien présenté et bien indenté facilite grandement sa lecture et sa compréhension. Elles sont toutes exécutées, dans l’ordre ou elles sont écrites.

Considérons par exemple l’algorithme suivant, calculant les trois premières puissances d’un nombre :

def puissances(x):
    """
    :entrée x:  réel
    :pré-cond:  Ø
    :sortie p2: réel
    :sortie p3: réel
    :sortie p4: réel
    :post-cond: p2 = x², p3 = x³, p4 = x⁴
    """
    p2 = x*x
    p3 = p2*x
    p4 = p3*x
    return p2, p3, p4

On peut représenter l’enchaînement des instructions de cet algorithme par le diagramme ci-dessous :

digraph sequence {
  rankdir=LR; splines=ortho
  node [ shape=box ]
  i1 [ label="p2 = x*x" ]
  i2 [ label="p3 = p2*x" ]
  i3 [ label="p4 = p3*x" ]
  i4 [ label="return p2, p3, p4"]
  i1 -> i2 -> i3 -> i4
}

Condition

Dans certains cas, il est nécessaire d’exécuter des instructions différentes selon qu’une condition est remplie ou non. Dans ce cas, on utilisera un enchaînement conditionnel. Celui-ci est composé ainsi :

  • première ligne :
    • le mot-clé if,
    • l’expression booléenne représentant la condition,
    • le caractère :.
  • enchaînement d’insructions à exécuter si la condition est vraie
    • toutes précédées de quatre espaces de plus que la première ligne.
  • le mot-clé else:
    • précédé du même nombre d’espaces que la première ligne.
  • enchaînement d’instructions à exécuter si la condition est fausse
    • toutes précédées de quatre espaces de plus que la première ligne.

NB : la clause else et les points suivants sont facultatifs.

Les enchaînements suivant le if et le else ne sont bien sûr pas limités à des enchaînements séquentiels. Il peuvent comporter d’autres enchaînements conditionnels, ou des enchaînements répétitifs (cf. ci-dessous). La première instruction précédée du même nombre d’espaces (ou moins) que la première ligne (la ligne du if) sera reconnue comme ne faisant pas partie de l’enchaînement conditionnel. Elle sera donc exécutée que la condition soit vraie ou non.

Considérons par exemple l’algorithme suivant, qui calcule pour un nombre donné sa valeur absolue et son signe (représenté par le nombre 1 ou -1) :

def valabs_et_signe(x):
    """
    :entrée x: réel
    :pré-cond: Ø
    :sortie signe: entier
    :sortie valabs: réel
    :post-cond: signe = 1 si x ≥ 0, -1 sinon
    :post-cond: valabs = |x|
    """
    if x >= 0:
        signe = 1
        valabs = x
    else:
        signe = -1
        valabs = -x
    return signe, valabs

On peut représenter l’enchaînement des instructions de cet algorithme par le diagramme ci-dessous :

digraph condition {
  rankdir=LR; splines=ortho
  node [ shape=box ]
  t1 [ label="x >= 0", shape=diamond ]
  i1 [ label="signe = 1" ]
  i2 [ label="valabs = x" ]
  i3 [ label="signe = -1" ]
  i4 [ label="valabs = -x" ]
  j1 [ label="", shape=none, width=0, height=0 ]
  i5 [ label="return signe, valabs" ]
  t1 -> i1 [ taillabel="oui" ]
  i1 -> i2; i2 -> j1 [ arrowhead=none ]
  t1 -> i3 [ taillabel="non" ]
  i3 -> i4; i4 -> j1 [ arrowhead=none ]
  j1 -> i5
}

Conditions multiples

Afin d’éviter d’imbriquer les if et les else les uns dans les autres, Python propose l’instruction elif (contraction de else if) qui se traduit en langage courant par “sinon si”.

Le elif doit être utilisé avec précaution et parcimonie, uniquement lorsque l’on est certain que toutes les conditions sont exclusives. En effet, les conditions sont vérifiées les unes après les autres jusqu’à ce qu’une condition soit vraie. Si une condition est vérifiée, les suivantes dans la liste ne sont pas examinées.

Une suite de elif peut se terminer par un else qui sera donc traité si aucune condition n’a été vérifiée jusque là.

Voici un exemple d’utilisation du elif :

def mention(note):
    """
    :entrée note: réel
    :pré-cond: 0 <= note <= 20
    :sortie: chaîne de caractères
    :post-cond: affiche "recalé" si la note est inférieure à 10,
                très bien si elle est supérieure à 16 et bien
                le reste du temps
    """
    if note <= 10:
        print("recalé")
    elif note <=16
        print("bien")
    else
        print("très bien")

Répétition

Python supporte deux types d’enchaînements répétitifs, également appelés « enchaînements itératifs » ou « boucles » : la boucle while et la boucle for.

La boucle while

Dans certains cas, il est nécessaire d’exécuter les mêmes instructions un nombre de fois variable selon les valeurs d’entrées de l’algorithme. Plus précisément, on répétera ces instructions tant qu’une condition est remplie. Dans ce cas, on utilisera une boucle while, qui est composée ainsi :

  • première ligne :
    • le mot-clé while,
    • l’expression booléenne représentant la condition,
    • le caractère :.
  • enchaînement d’instructions à exécuter tant que la condition est vraie
    • toutes précédées de quatre espaces de plus que la première ligne.

Ici encore, l’enchaînement d’instructions suivant le while peut comporter tous types d’enchaînements (séquentiel, conditionnel, répétitif). La première instruction précédée du même nombre d’espaces (ou moins) que la première ligne (la ligne du while) sera reconnue comme ne faisant pas partie de la boucle. Elle sera donc exécutée dès que la condition devient fausse.

Considérons par exemple l’algorithme suivant qui calcule le nombre de chiffres (en base 10) nécessaires à l’écriture d’un entier positif :

def nb_chiffres(n):
    """
    :entrée n: entier
    :pré-cond: n > 0
    :sortie c: entier
    :post-cond: n s'écrit en base 10 avec c chiffres
    """
    c = 1
    while n > 10:
        c = c+1
        n = n//10
    return c

On peut représenter l’enchaînement des instructions de cet algorithme par le diagramme ci-dessous :

digraph boucle_while {
  rankdir=LR; splines=ortho
  node [ shape=box ]
  i1 [ label="c = 1" ]
  t1 [ label="n > 10", shape=diamond ]
  i2 [ label="c = c+1" ]
  i3 [ label="n = n//10" ]
  i4 [ label="return c" ]
  i1 -> t1
  t1 -> i2 [ taillabel="oui" ]
  i2 -> i3
  i3 -> i4 [ style=invis ]
  i3 -> t1 [ constraint=false ]
  t1 -> i4 [ taillabel="non"; constraint=false ]
}

Il est intéressant de remarquer que, selon les valeurs en entrée, les instructions de la boucle peuvent être exécutée plusieurs fois, une seule fois, voire pas du tout.

La boucle for

Certaines valeurs manipulées par un algorithme peuvent être vues comme « contenant » d’autres valeurs ; par exemple, une chaîne de caractères peut être vue comme une liste d’éléments plus simples que sont chacun de ses caractères. Ces valeurs sont qualifiées d’itérables en Python (c’est-à-dire que l’on peut itérer dessus, autrement dit, les parcourir une par une dans l’ordre, et appliquer le même ensemble d’actions sur chacune d’entre elles), et peuvent être utilisées avec la boucle for, qui est composée ainsi :

  • première ligne :
    • le mot-clé for,
    • un nom de variable
    • le mot-clé in,
    • l’itérable sur lequel on veut boucler
    • le caractère :.
  • enchaînement d’instructions à exécuter pour chaque élément de l’itérable
    • toutes précédées de quatre espaces de plus que la première ligne.

La première instruction précédée du même nombre d’espaces (ou moins) que la première ligne (la ligne du for) sera reconnue comme ne faisant pas partie de la boucle.

La variable nommée après le for prendra successivement pour valeur chacun des éléments de l’itérable; et pour chacun d’entre eux, les instructions de la boucle seront exécutées.

Considérons par exemple l’algorithme suivant qui retourne la chaîne de caractères « miroir » de la chaîne passée en entrée :

def miroir(s):
    """
    :entrée s: chaîne de caractères
    :pré-cond: Ø
    :sortie r: chaîne de caractères
    :post-cond: r est la chaîne miroir de s
    """
    r = ""
    for c in s:
       r = c+r
    return r

Il existe un cas particulier de boucle extrêmement fréquent : une boucle itérant sur des entiers successifs. Pour répondre à ce besoin, Python fournit la fonction range, qui fournit un itérable contenant une séquence d’entier. Plus précisément :

  • range(i) itère sur l’intervalle [0,i[
  • range(i, j) itère sur l’intervalle [i,j[

Note

Notez à nouveau l’interprétation des bornes en Python. La borne inférieure est toujours inclue, la borne supérieure est toujours exclue.

Considérons par exemple l’algorithme suivant qui retourne la factorielle de l’entier n :

def factorielle(n):
    """
    :entrée n:  entier
    :pré-cond:  n ≥ 0
    :sortie f:  entier
    :post-cond: f = n! = 1×2×3×...×(n-1)×n
    """
    f = 1
    for i in range(1, n+1):
        f = f*i
    return f

Note

Il est toujours possible de remplacer une boucle for par une boucle while. Cependant, dans de nombreux cas, la boucle for est plus lisible.

Appel de fonction

Une fois que l’on a totalement défini une fonction, cette dernière fait alors partie des « capacités » de l’ordinateur.

Pour indiquer à l’ordinateur qu’il doit appeler (ou utiliser) une fonction, on écrira une affectation dont :

  • la partie gauche comportera autant de variables que la fonction comporte de paramètres de sorties ;
  • la partie droite est constituée du nom de la fonction, suivi par la liste des valeurs des paramètres d’entrée, entre parenthèses et séparées, le cas échéant, par une virgule.

Par exemple :

>>> i, j, k = puissances(8)
>>> k = bonbons_restants(2*j, i-1)

Dans les exemples ci-dessus, on voit que les valeurs passées aux paramètres d’entrée peuvent être des expressions complexes. On constate aussi que les variables recevant les valeurs des paramètres de sortie ne sont pas tenues d’avoir le même nom que ces paramètres ; c’est l’ordre des variables qui détermine leur correspondance avec les paramètres de sortie.

Enfin, notons que, dans le cas particulier des fonctions n’ayant qu’un seul paramètre de sortie, l’appel à la fonction peut être utilisé directement dans une expression. Par exemple, au lieu d’écrire :

>>> i = bonbons_restants(10,3)
>>> j = 2 * i

on peut écrire directement :

>>> j = 2 * bonbons_restants(10, 3)

Dans le cas d’une procédure, qui ne retourne aucune valeur, l’appel se limitera au nom de la procédure suivi de ses paramètres :

>>> print("bonjour le monde")

Portée des variables

Les variables utilisées dans une fonction sont propres à cette fonction. Elles ne sont ni visibles, ni utilisables depuis d’autres fonctions, même si ces dernière définissent une variable ayant le même nom. On dit que la portée de la variable est limitée à la fonction qui la définit [4].

Considérons l’exemple ci-dessous :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def total_chiffres_fact(n):
    """
    :entrée n:  entier
    :pré-cond:  n > 0
    :sortie c:  entier
    :post-cond: c et le nombre total de chiffre nécessaire pour écrire les
                factorielles de tous les entiers entre 1 et n
    """
    f = 1
    c = 0
    for i in range(1, n+1):
        f = f*i
        c = c+nb_chiffres(f)
    return c

Cette fonction appelle la fonction nb_chiffres définie plus haut. On remarquera que ces deux fonctions utilisent les mêmes noms de variable (n, c). Cependant, il n’y a aucune interaction entre la valeur de n (respectivement de c) dans nb_chiffres et la valeur de n (respectivement de c) dans total_chiffres_fact.

Il faut imaginer que, lorsque l’ordinateur exécute total_chiffres_fact, il stocke les valeurs de n, c et f dans un emplacement E1 de sa mémoire, emplacement dédié aux variables “internes” à total_chiffres_fact\\. Lorsqu'à la ligne 13, on lui demande d'exécuter la fonction ``nb_chiffres, il laisse de coté l’emplacement E1 et commence à travailler sur un emplacement E2, dédié aux variables internes de nb_chiffres dans lequel il va stocker les valeurs de n et c de la fonction nb_chiffres. Lorsque cette dernière se termine, l’emplacement E2 est supprimé, et l’ordinateur travaille à nouveau sur l’emplacement E1 pour terminer l’exécution de total_chiffres_fact.

../_images/porteeVariables.png

Vous pouvez faire la simulation sur Pythontutor pour mieux comprendre comment sont gérés les espaces mémoires (http://pythontutor.com).

Il est cependant important de détailler ce qui se passe aux moments du passage de E1 à E2, et du retour de E2 à E1 :

  • au moment de passer de la fonction appelante (E1) à la fonction appelée (E2), les expressions passées aux paramètres d’entrée (entre les parenthèses) sont calculées avec les variables de E1, et leurs valeurs sont affectées aux variables correspondantes dans E2 ;
  • au moment de revenir de la fonction appelée à la fonction appelante, les valeurs des paramètres de sortie sont affectées aux variables correspondantes dans E1, ou substituées à l’appel de fonction si celui-ci est utilisé directement dans une expression [5].

Considérons l’exemple ci-dessus, où on aurait appelé la fonction total_chiffres_fact avec n=5. Au premier passage à la ligne 13, les variables de total_chiffres_fact ont les valeurs suivantes : n=5, c=0, i=1 et f=1. À ce moment, l’ordinateur calcule les valeurs à passer aux paramètres d’entrée de nb_chiffres, en l’occurrence un seul paramètre, dont la valeur est 1 (valeur de f). L’emplacement mémoire qui contiendra les variables de nb_chiffres est donc initialisé avec n=1. À la fin de l’exécution de nb_chiffres, ses variables ont pour valeur n=1 et c=1. Comme nb_chiffres est utilisée directement dans une expression, la valeur de paramètre de sortie c est substituée à l’appel de fonction, donc la ligne 13 de total_chiffres_fact revient ici à calculer :

c = c+1

dans l’emplacement mémoire de total_chiffres_fact, donc avec c=0. Notons aussi que la valeur de n dans total_chiffres_fact est toujours 5, et n’a pas été influencée par le fait que nb_chiffres utilisait une variable du même nom avec une valeur différente.

L’apparente complexité de ce processus est en fait une simplification : elle permet au programmeur d’une fonction f de ne pas se soucier des noms de variables utilisés dans les autres fonctions (celles appelées par f comme celles qui appellent f). Il favorise donc la modularité du code.

Notes de bas de page

[1]

En fait, peu importe le nombre d’espaces, mais toutes les lignes suivant la première doivent être indentées de la même manière, pour marquer leur appartenance à la définition de l’algorithme.

La première ligne non-indentée sera considérée comme ne faisant plus partie de cette définition.

On verra dans la suite du chapitre que toute la structure des algorithmes est ainsi définie par l’indentation.

[2]On dit d’ailleurs couramment qu’une fonction retourne ses paramètres de sortie.
[3]Ceci n’est en rien une contrainte technique, mais plutôt une règle de bonne pratique (cf. Bonnes pratiques de programmation). Et même cette règle peut, dans certains cas, souffrir des exceptions.
[4]Il est possible, en Python et d’en d’autres langages, de définir des variables avec une portée plus petite ou plus grande, mais nous ne traiterons pas de cela dans ce cours.
[5]En fait, c’est l’expression passée à return qui est systématiquement substituée à l’appel de la fonction. Python autorise une utilisation beaucoup plus souple de l’instruction return que celle préconisée dans ce cours.