En lisant les productions de mes étudiants, je rencontre une pratique minoritaire mais néanmoins assez répandue qui consiste à systématiquement définir toutes les variables d'une fonction au début de celle-ci. Voyons d'où peut provenir cette habitude et pourquoi elle n'est en général pas pertinente.

La faute à Pascal ?

Il est possible que la croyance qu'il soit nécessaire de définir toutes les variables au début d'une fonction ou d'un programme provienne des contraintes qui existaient dans l'ancienne façon de présenter les algorithmes au lycée, dont j'ai déjà parlé. Elle distingue trois ou quatre sections successives : les variables (pêle-mêle ce qui tient lieu de paramètres et les variables locales), les entrées, le traitement, les sorties.

Exercice d'algorithmique du bac 2016
L'épouvantable présentation des algorithmes au bac jusqu'en 2017.

Cette présentation ressemble à celle du langage Pascal, qui a été utilisé dans l'enseignement en France dans les années 1980. Chaque programme débute par une section de déclaration des variables.

function nombreRacines(a, b, c : Integer) : Integer;
var
  disc : Integer;
begin
  disc := b*b - 4*a*c;
  if disc > 0 then
    nombreRacines := 2
  else if disc = 0 then
    nombreRacines := 1
  else
    nombreRacines := 0;
end;
Une fonction Pascal qui donne le nombre de racines réelles de \(aX^2+bX+c\) (\(a\neq0\)).

Une des raisons de cette restriction syntaxique était, dit-on, que ça simplifiait l'analyse du programme par le compilateur : une raison essentiellement technique, donc.

D'autres raisons de vouloir systématiquement rassembler les déclarations de variables en début de programme ou fonction même si le langage n'y oblige pas sont :

  • lorsqu'il s'agit de variables globales, mais les variables globales c'est le mal ;
  • lorsque la fonction est très longue et qu'on aurait du mal à s'y retrouver, mais alors c'est que la fonction doit être scindée en des fonctions plus simples.

Problèmes liés à la définition initiale systématique

Valeur initiale dénuée de sens

En Python, il n'est pas possible de simplement déclarer une variable, c'est-à-dire, comme on l'a fait dans le programme en Pascal ci-dessus, indiquer qu'elle va exister et, éventuellement ou obligatoirement (selon le langage de programmation), dire son type. Non, en Python, on ne peut introduire une variable qu'en la définissant, c'est-à-dire en lui donnant une valeur initiale.

Mais lorsque la fonction qu'on écrit enchaine plusieurs étapes, il est souvent délicat de donner dès le début une valeur aux variables qui en réalité ne servent qu'à la deuxième étape. Dans le meilleur des cas, on peut donner une valeur neutre comme 0, mais la personne qui lit le programme se demandera à quoi rime cette variable que ne semble pas utilisée au début. Dans le pire cas, on doit affecter une valeur absconse et parfois on commet l'erreur de prendre une valeur qui n'a pas toujours de sens, comme L[0] alors qu'on n'a pas encore déterminé que L est non vide.

C'est également le cas lorsqu'une variable est d'intérêt purement local, comme ci-dessous.

def tri_bulle(t) :
  temp = 0  # Que signifie ce 0 ? Rien.
            # Écrire t[0] est une erreur pire car on ne sait pas si t est vide ou non
  for i in range(1, len(t)) :
    for j in range(0, len(t)-i) :
       if t[j] > t[j+1] :
          temp = t[j]
          t[j] = t[j+1]
          t[j+1] = temp
Une implémentation du tri bulle avec définition parasite initiale.

Mauvaise imbrication de boucles

L'habitude de définir les variables systématiquement en début de programme est une véritable peau de banane quand on doit imbriquer deux boucles.

Considérons le problème suivant, issu d'un devoir donné en MPSI. Soit L une liste non vide de listes d'entiers, par exemple [[3, 1, 2], [2, 4, 5, 3], [0, 1, 3, 4, 2]]. On souhaite constituer la liste des entiers qui apparaissent dans chacune de ces listes, dans l'exemple [3, 2]. Un algorithme fréquemment imaginé par les élèves pour résoudre ce problème consiste à énumérer chacun des éléments de la première liste et à vérifier s'il apparait dans toutes les autres, et cela en comptant le nombre de listes où il apparait et en vérifiant qu'il y en a le bon nombre. On peut critiquer cet algorithme et vouloir l'optimiser, mais il n'est pas déraisonnable.

La façon naturelle de traduire cette idée en Python est de n'introduire la variable qui sert de compteur qu'au moment où on commence à compter (dans mon DS, je faisais d'abord écrire une fonction pour vérifier l'appartenance d'un entier à une liste) :

def communs(L) :
  resultat = []
  for x in L[0] :  # L[0] existe car L est non vide
    c = 0
    for k in range(1, len(L)) :
       if appartient(x, L[k]) :
          c = c + 1
    if c == len(L) - 1 :
      resultat.append(x)
  return resultat
Traduction correcte de l'algorithme décrit.

Trop souvent, des étudiants qui ont pris l'habitude de définir toutes les variables au début écrivent le programme faux suivant :

def communs(L) :
  resultat = []
  c = 0
  for x in L[0] :  # L[0] existe car L est non vide
    for k in range(1, len(L)) :
       if appartient(x, L[k]) :
          c = c + 1
    if c == len(L) - 1 :
      resultat.append(x)
  return resultat
Erreur d'imbrication des boucles.

On trouve aussi le programme suivant, qui est correct mais pas judicieux (avec parfois le premier c = 0 rajouté après coup) :

def communs(L) :
  resultat = []
  c = 0
  for x in L[0] :  # L[0] existe car L est non vide
    for k in range(1, len(L)) :
       if appartient(x, L[k]) :
          c = c + 1
    if c == len(L) - 1 :
      resultat.append(x)
    c = 0
  return resultat
Une version qui fonctionne mais laborieusement.

Désorganisation de la réflexion

Les différentes versions fautives à des degrés divers montrent en outre, sauf pour les étudiants qui rajoutent une ligne au début après coup, que cette contrainte imaginaire perturbent la conception du programme en Python : alors que l'algorithme comprend deux opérations imbriquées mais distinctes, leur réalisation est entrelacée et l'esprit s'occupe de la deuxième dès le début de la première.

Complication des invariants de boucle

Cet entrelacement inutile a aussi des conséquences si on veut analyser le programme. En effet, pour établir la correction du dernier programme ci-dessus, il est nécessaire d'ajouter à l'invariant de la boucle externe le fait que c vaut zéro, de façon à assurer la validité initiale de l'invariant qui justifie la correction de la boucle interne de comptage. Si on a rédigé le programme comme je l'ai proposé initialement, l'affectation c = 0 se trouve au début du corps de la boucle externe : elle apparait donc comme interne au corps de cette boucle et on peut donc s'abstenir de parler de c dans l'invariant de cette boucle.