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. Lire la suite


Le nouveau programme de seconde préconise de programmer en écrivant des fonctions. Un des aspects intéressants des fonctions en Python, comme dans la plupart des langages de programmation, est que les variables d'une fonction sont locales à cette fonction. Dans ce billet, j'explique ce que cela signifie et comment les choses se passent lors de l'exécution d'un appel de fonction.

Cela permet également de comprendre les possibilités qui existent lorsqu'on écrit une fonction dont un paramètre est une liste (une structure dite mutable). Bien que l'étude des listes ne soit pas au programme, on pourra, nous dit le document d'accompagnement, être amené à s'en servir ponctuellement. Il est donc judicieux pour le professeur de connaitre les problèmes qui peuvent survenir lors de leur utilisation.

Contextes et tas

Pour comprendre l'exécution d'une fonction, il faut avoir une représentation de la façon dont la mémoire est conceptuellement organisée par Python. Pour aller vite, chacune des différentes valeurs qu'on manipule (l'entier 2, le flottant 2.0, le booléen True, la chaine "True", etc.) est représentée en mémoire par ce que j'appellerai un objet : lorsqu'on calcule 32 + 10, cela donne lieu, in fine, a la création d'un objet qui représente l'entier 42. Ces objets résident dans une partie de la mémoire qu'on appelle le tas.

Lorsqu'on procède à une affectation, on convient d'un nom pour désigner une valeur. Ainsi, effectuer x = 32 + 10 consiste d'abord à calculer la valeur de l'expression 32 + 10 (soit un objet qui représente 42) puis à mémoriser que le nom x désigne cette valeur (cet objet). L'ensemble de ces désignations forme un contexte, et les expressions sont évaluées en référence à un contexte. C'est ainsi qu'après avoir effectué l'affectation ci-dessus, l'expression x + 1 s'évalue en l'entier 43.

Il y a plusieurs contextes qui peuvent exister ; j'y reviendrai. Il y a notamment un contexte qu'on dit parfois global ou « de la console » : c'est lui qui mémorise les associations découlant des affectations qui sont effectuées directement dans la console Python. En revanche, il n'y a qu'un seul tas.

Lorsque je présente cela en cours de MPSI/PCSI, à la mi-septembre, je dessine une petite étoile au tableau dans la partie « Tas » pour chaque objet (avec la valeur qu'il matérialise) et dans la partie « Contexte global » j'inscris le nom de chaque variable avec une flèche qui pointe vers l'étoile correspondante dans le tas (c'est analogue à ce que fait Pythontutor). J'ai la flemme de faire des dessins ici, mais c'est important d'avoir cette représentation en tête (ou sur papier) pour suivre la suite. Quand on fait une nouvelle affectation à la même variable, on raye ou efface l'ancienne flèche pour la remplacer par une nouvelle qui pointe vers la nouvelle valeur.

Contexte d'appel et localité des variables

Considérons la fonction suivante, qui renvoie le nombre de racines réelles du trinôme \(aX^2+bX+c\).

def nb_racines(a, b, c) :
  d = b**2 - 4*a*c
  if d > 0 :
    return 2
  elif d == 0 :
    return 1
  else :
    return 0

Cette fonction présente une variable, locale, dont l'intérêt est d'éviter de calculer deux fois le discriminant, ce qui se produirait si on reproduisait la formule dans la condition du if et dans celle du elif.

Que se passe-t-il si, après avoir défini cette fonction, on effectue successivement dans la console les opérations suivantes ?

d = 1792
nb_racines(1, 45, 27)
d

En particulier, est-ce que d vaut toujours 1792, ou a-t-il pris la valeur du discriminant de \(X^2+45X+27\), soit 1917 ? Et d'ailleurs, comment est-ce que le code de la fonction, qu'on a écrit pour des paramètres formels a, b et c, s'exécute avec les paramètres effectifs de notre appel, 1, 45 et 27 ?

La réponse à ces deux questions est dans les contextes. L'affectation d = 1792 se fait dans le contexte de la console. Mais pour chaque appel de fonction, Python crée un contexte spécifique (avant cela, il calcule la valeur de chacun des paramètres effectifs). C'est pour ça qu'on peut faire nb_racines(1, 42 + 3, 27) voire, soyons fous nb_racines(nb_racines(1, 2, 1), 42 + 3, 27). Et c'est pour ça que les variables sont locales.

Concrètement, un contexte est créé pour l'appel, dans lequel le nom de chaque paramètre formel est automatiquement associé à la valeur de chacun des paramètres effectifs. Et l'appel de fonction s'exécute dans ce contexte, ce qui signifie notamment que l'affectation d = b**2 - 4*a*c se fait dans ce contexte, sans toucher à la variable d du contexte de la console. Quand l'appel se termine, le contexte est détruit.

Dans la représentation avec des flèches, vous avez un contexte global par exemple en bleu un contexte spécifique en rouge et le tas en noir. Donc on distingue le d bleu du d rouge et chacun a une flèche qui pointe vers une étoile noire dans le tas (éventuellement la même).

La spécificité du contexte d'appel permet d'imbriquer des appels de fonction sans difficulté et permet la localité des variables, qui est très pratique. Comparons la situation avec celles de certaines calculatrices où les variables sont globales : quand on écrit un programme, on doit faire attention à ne pas utiliser les mêmes variables que dans un autre ; quelle idiotie ! En Python, chaque fonction a ses variables, c'est beaucoup plus pratique.

Mutabilité

Introduction

Les listes-de-Python sont une structure qui permet de rassembler plusieurs données. Par exemple, on peut considérer une liste de mesures physiques, ou la liste d'entiers suivante, qu'on nomme L dans le contexte de la console :

L = [1792, 1848, 1871, 1917, 1936, 1968, 1981, 2005]

Entre autres choses, on peut connaitre le nombre d'éléments de la liste en évaluant l'expression len(L) et on peut aussi lire chacune des valeurs contenues grâce à un numéro d'indice qui commence à zéro. Ainsi l'expression L[1] vaut 1848, etc.

On peut également modifier le contenu de chaque case (en faisant par exemple L[0] = 1793), et c'est pour cela qu'on dit que la structure est mutable.

Dans la représentation graphique de la mémoire, on a une nouveauté : la liste est un objet du tas qui lui même est l'origine de flèches (ici 8) qui pointent vers d'autres objets du tas, et ces flèches peuvent être remplacées par d'autres.

C'est très pratique mais les conséquences sont nombreuses : le tas étant commun, on a maintenant des flèches communes, alors qu'avec les contextes chaque appel de fonction avait ses flèches.

Le problème de l'alias

Chaque fois qu'on évalue une expression de la forme [1, 2, 3], on provoque la création dans le tas d'une nouvelle liste, distincte de toutes les autres (même si son contenu peut être identique à celui d'une autre). Ainsi, après avoir effectué

11391590845c694aa43a88d_000008 Lire la suite


Le document d'accompagnement pour l'algorithmique et la programmation en seconde mentionne page 13 :

Un ordinateur ne travaille pas avec des nombres réels, mais avec des flottants, c’est-à-dire un sous-ensemble des nombres décimaux dont la précision est limitée par des contraintes liées au codage en mémoire.

C’est ainsi qu’en Python, le test d’égalité 3+10**(-16)==3 s’évalue en True alors que le test 3+10**(-15)==3 s’évalue en False. On retiendra qu’il faut éviter de tester l’égalité entre deux flottants, et préférer la recherche d’une précision donnée. En revanche, bien sûr, il n’y a aucun problème à comparer deux nombres entiers.

C'est tout à fait exact et le programme de seconde ne demande pas d'enseigner les subtilités des flottants aux élèves. Cependant, au cours de leurs manipulations, les élèves peuvent obtenir des résultats qui semblent aberrants si on assimile les flottants aux réels, comme dans l'exemple cité. Également, on constate que 1.1 + 1.1 donne 2.2 mais que 1.1 + 1.1 + 1.1 donne 3.3000000000000003. Il peut être utile au professeur d'en savoir un peu plus afin de pouvoir en dire suffisamment aux élèves pour que l'informatique et les résultats qu'elle donne n'apparaissent pas comme quelque chose d'ésotérique. Cela permet aussi de comprendre pourquoi certaines façons de faire qu'on pouvait être contraint d'utiliser avec certaines calculatrices limitées sont à bannir.