Python Portée

Identification

Infoforall

17 - Portée des variables locales


Nous avons vu que la gestion des variables peut être vue comme une simple affaire de boîtes sur les cas simples.

représentation en boîte

Mais cette version simple ne suffit pas pour tout expliquer.

Vous allez voir aujourd'hui pourquoi il est inutile de donner des noms différents aux paramètres de plusieurs fonctions. Sur l'exemple ci-dessous, Python ne fera aucune confusion entre les trois versions du paramètre feutre :

1 2 3
def deplacer(feutre, x, y) : def trois(feutre, distance, angle) : def triangle(feutre, cote) :

Heureusement car sinon, il faudrait à chaque fois donner un nom un peu différent comme sur cet exemple :

1 2 3
def deplacer(feutre, x, y) : def trois(ftr, distance, angle) : def triangle(f, cote) :

Vous imaginez si on devait se souvenir des noms de toutes les variables et paramètres utilisés ailleurs dans le programme... Il n'y aurait qu'un seul informaticien au monde et il ressemblerait à...

Sauron voit tout
L'Oeil de Sauron : Gif provenant de https://gifer.com
Tour de Sauron
Bienvenue au Mordor ! Image CC Public Domain Certification

Logiciel nécessaire pour l'activité : Python 3

Evaluation ✎ : question 04-05-07-18

Documents de cours : open document ou pdf

1 - Variables du programme et variables des fonctions

Le modèle de la variable-boîte est simple mais il ne permet pas d'expliquer certains cas plus complexes en Python.

01° Sans lancer le programme ci-dessous, répondre aux questions suivantes en utilisant juste votre intuition :

  • L1 : La fonction modification() va-t-elle se mettre en mémoire ou automatiquement s'exécuter ?
  • L4 : Que contient, à votre avis, la variable a lorsqu'on exécute la ligne 4 ?
  • L5 : Que contient, à votre avis, la variable a après avoir exécuté la ligne 5 ?
1 2 3 4 5
def modification(a): a = 5 a = 10 modification(a)

...CORRECTION...

La fonction ne s'exécute pas automatique en ligne 1 : il s'agit juste d'une déclaration. On met la fonction en mémoire et à partir de maintenant, on pourra y faire appel.

La variable a contient visiblement 10.

On lance l'appel à la fonction en lui transmettant a qu'on stocke dans un paramètre que se nomme également a ! Du coup, on pourrait croire qu'en exécutant la ligne 2, la variable a qu'on avait créé ligne 5 soit maintenant affectée à 5.

Attention, je spoile : ce n'est pas ce qui va se passer.

02° Lancer le programme. Visualiser le contenu de la variable a soit directement dans le menu VARIABLES de Thonny, soit en tapant a dans la console.

Surprise !

Voilà pourquoi nous allons devoir regarder d'un peu plus près comment les variables semblent gérées.

Après exécution, nous obtenons ceci :

>>> a 10

Et oui, le passage dans la fonction n'a pas modifié la variable a !

Le but de cette activité est de vous faire comprendre pourquoi.

03° Rajouter les indications (visibles en gris) dans votre tête lors de la lecture du programme permet-il d'obtenir le bon résultat ?

1 2 3 4 5
def modification(a_de_la_fonction): a_de_la_fonction = 5 a_du_programme_principal = 10 modification(a_du_programme_principal)

...CORRECTION...

Oui, plus aucun problème. On ne peut plus confondre les deux.La variable a_du_programme_principal contient visiblement 10.

On lance l'appel à la fonction en lui transmettant a_du_programme_principal qu'on stocke dans un paramètre qui se nomme a_de_la_fonction ! Ensuite, on voit clairement que c'est la variable a_de_la_fonction qui est affectée à la valeur 5.

Du coup, cela ne modifie pas la variable a_du_programme_principal.

Variables globales et variables locales

On appelle variable locale une variable qui est définie dans le corps d'une fonction.

On appelle variable globale une variable qui est définie dans le corps du programme principal.

Exemple

En lignes 1 et 2, la variable nommée a est une variable locale à la fonction.

En lignes 4 et 5, la variable nommée a est une variable globale au programme. Il ne s'agit donc pas de la même que celle des lignes 1 et 2.

1 2 3 4 5
def modification(a): a = 5 a = 10 modification(a)

✎ 04° Notez (sans justification particulière) pour chacune des variables surlignées dans le programme ci-dessous s'il s'agit d'une variable locale ou d'une variable globale.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
import random as rd def multiplication(max_a, max_b): """Fonction qui tire au hasard deux nombres et demande à l'utilisateur le résultat de leur multiplication, tant que le résultat n'est pas bon ! ::param max_a(int) :: la valeur maximale du premier tirage ::param max_b(int) :: la valeur maximale du deuxième tirage ::return (int) : renvoie le nombre essai de tentatives """ a = rd.randint(1, max_a) b = rd.randint(1, max_b) m = 0 # m contiendra la valeur que l'utilisateur pense être a*b essai = 0 while m != x*y: m = input(f"Que vaut {a} x {b} = ? : ") m = int(m) essai = essai + 1 return essai print("Donne le résultat des multiplications suivantes.") nbr = 0 for x in range(10): nbr = multiplication(10,10) + nbr print(f"{nbr} tentatives pour trouver 10 multiplications !")

✎ 05° Expliquez pourquoi renommer les variables a et b respectivement en x et y sur les lignes 11-12-16 ne poserait aucun problème alors qu'il existe déjà une variable nommée x en ligne 23.

Maintenant que vous avez vu, en tant qu'humain, qu'il s'agit de variables différentes, nous allons voir comment l'interpréteur Python parvient à ne pas les distinguer, en tant que simple machine.

2 - Identifiant des variables

Alias et Espace des noms

Une variable Python est en réalité un alias pour atteindre une zone mémoire sans avoir à connaître son adresse réelle.

Imaginons qu'on tape ceci dans la console :

>>> a = 5 >>> b = 9 >>> a 5

L'interpréteur Python crée un tableau qu'on appelle Espace des Noms permettant de faire le lien entre le nom d'une variable et une zone de la mémoire (adr dans les images ci-dessous).

Lorsque l'interpréteur Python tombe sur a, il sait que cela fait référence à la zone-mémoire 68 qui contient 5.

espace des noms

De la même façon, lorsque l'interpréteur Python tombe sur b, il sait que cela fait référence à la zone-mémoire 85 qui contient 9.

Fonction native id()

La fonction native id() fournit l'identifiant-mémoire qui permet de faire le lien entre une variable de l'epsace des noms et une zone-mémoire particulière.

espace des noms
>>> a = 5 >>> id(a) 68 >>> id(5) 68

Votre propre valeur peut être différente bien entendu.

06° Utiliser la fonction native id() pour visualiser qu'on peut bien faire la liaison entre une variable et un contenu-mémoire à l'aide de l'identifiant Python.

Attention : il est possible que vous n'obteniez pas les mêmes valeurs d'identifiants en fonction de l'implémentation de votre Python.

>>> a = 50 >>> id(a) 9201408 >>> id(50) 9201408 >>> b = 51 >>> id(b) 9201440 >>> id(51) ???

Pour la suite de cet exercice, je mets juste les 4 derniers chiffres en avant pour distinguer les variables. C'est plus facile pour notre cerveau que de regarder l'intégralité du nombre affiché.

✎ 07° Complétez l'espace des noms et l'état de la mémoire en utilisant la fonction native id() sur les variables a b c d e.

Début de rédaction de l'espace des noms

  • Variable a -> id 1408
  • Variable b -> id ...
  • ...

Début de rédaction de l'état de la mémoire

  • Id 1408 -> integer 50
  • Id ... -> integer 51
  • ...
>>> a = 50 >>> id(a) 9201408 >>> id(50) 9201408 >>> b = 51 >>> c = "Hello" >>> d = "World" >>> e = c + d

Vous allez maintenant pouvoir comprendre ce que fait Python lorsqu'il doit traiter une instruction un peu bizarre comme a = b.

08° Si on utilise une instruction comme a = b, est-ce a ou b qui est modifiée ?

...CORRECTION...

On évalue ce qui est à droite PUIS on l'affecte à gauche.

C'est donc la variable a qui sera modifiée.

09° En utilisant les résultats des instructions ci-dessous, expliquer si les variables a et b sont des alias vers une même zone-mémoire ou si l'instruction a réellement dupliqué les données.

>>> b = 50 >>> id(b) 9201408 >>> id(50) 9201408 >>> a = b >>> id(a) 9201408

...CORRECTION...

On voit clairement que a et a font référence à la même zone-mémoire 9201408 qui contient l'integer 50.

Il nous reste juste à voir ce qui se passe lorsqu'on utilise une nouvelle affectation sur une valeur qui existe déjà.

10° Exécuter les instructions ci-dessous. Lors des affectations successives, l'identifiant de la variable x reste-il identique ou est-il modifié ?

>>> x = 5 >>> id(x) ??? >>> x = x + 1 >>> id(x) ??? >>> x = 5 * x >>> id(x) ???

...CORRECTION...

>>> x = 5 >>> id(x) 9199968 >>> x = x + 1 >>> id(x) 9200000 >>> x = 5 * x >>> id(x) 9200768

On voit clairement qu'à chaque nouvelle affectation, l'identifiant associé à x change, pour référencer un nouveau contenu.

Bilan sur l'affectation (à comprendre)

Affectation d'une variable

Réaliser une affectation avec l'opérateur = associe simplement l'identifiant d'une zone-mémoire à cette variable. L'association se fait via un tableau qu'on nomme espace des noms.

Imaginons les quelques lignes suivantes :

>>> a = 5 >>> b = 9

Cela pourrait se représenter de cette façon :

espace des noms

Si vous voulez connaître l'identifiant associé à la variable a, il suffit d'utliser id(a). Si on considère l'exemple ci-dessus, la fonction va alors renvoyer 68.

Affectations différentes successives

Lorsqu'on réalise une nouvelle affectation sur une variable préexistante, on change simplement l'identifiant associé à la variable dans l'espace des noms.

Imaginons ceci qui correspond à l'exemple ci-dessus :

>>> a = 5 >>> a = 50

Création d'un alias

Lorsqu'on note b = a, on ne duplique pas le contenu de a dans b. Non, b et a font juste référence à la même zone mémoire.

On dit que b et a sont des alias de la même zone-mémoire.

>>> a = 50 >>> id(a) 99 >>> b = a >>> id(b) 99

Par contre, si on modifie le contenu d'une des deux variables, elles feront référence à deux zone-mémoires différentes ensuite.

>>> a = 50 >>> id(a) 99 >>> b = a >>> id(b) 99 >>> a = 5 >>> id(a) 68 >>> id(b) 99

3 - Espaces des noms

Notez bien que cette partie s'appelle espaces des noms, avec un s.

Nous allons voir qu'il n'existe pas un seul espace des noms mais plusieurs espaces des noms :

  • Le corps du programme possède son espace des noms
  • Chaque fonction possède son propre espace des noms
Portée des variables et espaces des noms

Les variables créées dans les fonctions ne peuvent être lues et modifiées qu'à l'intérieur de cette fonction. Ce sont des variables locales à la fonction.

Exemple : nous avons ici plusieurs variables note.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
def gentil_prof(note): if note < 10: note = note + 4 return note def mechant_prof(note): if note > 10: note = 10 + (note - 10) // 2 else : note = note // 2 return note note = 8 y = gentil_prof(note) z = mechant_prof(note)

Ici, il y a donc

  • note de l'espace des noms de la fonction gentil_prof()
  • note de l'espace des noms de la fonction mechant_prof()
  • note de l'espace des noms du programme

Pour comprendre la même chose que l'ordinateur, voilà comment vous devriez renommer dans votre tête les variables lorsqu'elles portent le même nom :

  1. Les variables note présentes dans la fonction gentil_prof() pourraient être renommées mentalement note_gp pour indiquer qu'il s'agit des variables note de la première fonction
  2. Les variables note présentes dans la fonction mechant_prof() pourraient être renommées mentalement note_mp pour indiquer qu'il s'agit des variables note de la deuxième fonction
  3. Les variables note présentes dans le corps du programme lui-même pourraient être renommées mentalement note_pp pour indiquer qu'il s'agit des variables note du programme principal
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
def gentil_prof(note_gp): if note_gp < 10: note_gp = note_gp + 4 return note_gp def mechant_prof(note_mp): if note_mp > 10: note_mp = 10 + (note_mp - 10) // 2 else : note_mp = note_mp // 2 return note_mp note_pp = 8 y = gentil_prof(note_pp) z = mechant_prof(note_pp)

Plus de confusion possibile entre les différents variables note.

Voici ce qu'on obtient en mémoire après lancement du programme :

>>> note 8 >>> y 12 >>> z 4

11° Exécuter mentalement les lignes 13 et 14 puis répondre aux questions.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
def gentil_prof(note_gp): if note_gp < 10: note_gp = note_gp + 4 return note_gp def mechant_prof(note_mp): if note_mp > 10: note_mp = 10 + (note_mp - 10) // 2 else : note_mp = note_mp // 2 return note_mp note_pp = 6 y = gentil_prof(note_pp) z = mechant_prof(note_pp)
  1. L14 : Quel est l'argument envoyé à la fonction gentil_prof() ?
  2. L01 : Quel est le nom du paramètre qui stocke cette valeur envoyée ?
  3. Que contient la variable note_pp après exécution de la ligne 14 ?
  4. Que contient la variable y après exécution de la ligne 14 ?

...CORRECTION...

Ligne 13 : x_pp = 6.

Ligne 14 : cela revient donc à faire y = gentil_prof(6)

On voit donc que l'argument envoyé est 6.

On stocke cet argument dans le paramètre note_gp.

Lors de cet appel, on affecte donc 6 à note_gp.

Comme cette valeur est inférieure à 10, on affecte (en ligne 3) la valeur 10 à la variable note_gp.

On renvoie ce 10 et, de retour en ligne 14, on stocke cette valeur dans y : y contient maintenant l'identifiant de 10.

En utilisant la notion d'espace des noms, il est donc maintenant clair qu'on n'a absolument pas touché à la variable x_pp qui fait toujours référence à 6.

12° Exécuter mentalement la ligne 15. Répondre aux questions.

  1. L15 : Quel est l'argument envoyé à la fonction mechant_prof() ?
  2. L06 : Dans quel paramètre stocke-t-on cet argument ?
  3. Que contient la variable note_pp après exécution ?
  4. Que contient la variable z après exécution ?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
def gentil_prof(note_gp): if note_gp < 10: note_gp = note_gp + 4 return note_gp def mechant_prof(note_mp): if note_mp > 10: note_mp = 10 + (note_mp - 10) // 2 else : note_mp = note_mp // 2 return note_mp note_pp = 6 y = gentil_prof(note_pp) z = mechant_prof(note_pp)

...CORRECTION...

L'argument envoyé est donc encore 6.

On stocke cet argument dans le paramètre note_mp.

Comme cette valeur est inférieure à 10, on affecte (en ligne 10) x2 à la valeur 0.

On renvoie ce 0 et on voit, ligne 13, qu'on l'affecte à la variable z.

En utilisant l'espace des noms, il est clair qu'on n'a absolument pas touché à la variable xpp qui fait toujours référence à -5.

Pourquoi l'espace des noms ?

Pourquoi avoir créé ce mécanisme complexe d'espaces des noms multiples ?

Deux raisons à connaître :

  1. Pour permettre d'utiliser plusieurs fois un même nom explicite de variables : si les espaces des noms n'existaient pas, il faudrait vraiment créer des scripts avec note_gentil, note_mechant et note_programme_principal. Finalement, l'espace des noms, c'est pas mal non ?
  2. Pour libérer de la mémoire : lorsqu'on sort d'une fonction, l'espace des noms de cette fonction disparaît et on libère ainsi de la mémoire. Cela veut dire qu'à part ce qu'on renvoie via le return, toutes les variables locales disparaissent une fois que la fonction a répondu.

4 - Portée des variables locales

Variables locales en Python

Vous devriez avoir compris qu'en Python :

  • Une variable locale est une variable déclarée à l'intérieur d'une fonction.
  • Une variable locale n'est lisible et modifiable qu'à l'intérieur de la fonction où elle est déclarée : c'est normal puisqu'elle est détruite une fois qu'on sort de la fonction avec return.
portee-variable-locale

Les paramètres sont bien des variables locales comme les autres si ce n'est qu'on les affecte uniquement lors de l'appel effectif de la fonction.

Les questions finales ci-dessous ne visent qu'à clarifier les indications précédentes. C'est en pratiquant qu'on comprend vraiment ce que tout cela veut dire. Aucune nouveauté.

13° Placer ces fonctions en mémoire.

1 2 3 4 5 6 7
def calcul(a, b): c = a + fois2(b) return c def fois2(d): e = d * 2 return e

Utiliser ensuite les instructions dans la console :

>>> calcul(2,3) 8 >>> c NameError: name 'c' is not defined

Question : pourquoi le programme ne parvient pas à lire c ?

...CORRECTION...

Les variables locales à une fonction ne sont lisibles que lors du déroulement de la fonction. Une fois hors de la fonction, Python ne vous donne pas accès à ces variables locales car l'espace des noms de la fonction n'existe plus.

C'est le cas ici avec c qui appartient à l'espace des noms de la fonction calcul().

Espace des noms après la réponse de la fonction

Il faut donc bien avoir ceci à l'esprit : l'espace des noms d'une fonction est détruit dès que la fonction est terminée.

Pour vous aider un peu plus à visualiser tout cela, nous allons utiliser un outil en ligne bien pratique : http://pythontutor.com.

14° Partir sur le site Python Tutor et choisir Visualize your code and get live help now.

  • Rentrer le code fourni ci-dessous.
  • 1 2 3 4 5 6 7 8 9
    def calcul(a, b): c = a + fois2(b) return c def fois2(d): e = d * 2 return e calcul(3, 2)
  • Cliquez sur Visualize Execution
  • Cliquez sur Forward (en avant, vers l'avant...) pour visualiser la création/destruction des variables au cours de l'exécution du programme.

15° Vous avez ci-dessous une copie d'écran obtenu sur Python Tutor. Le contenu mémoire est celui obtenu pendant l'appel de la fonction calcul(3,2) juste avant l'exécution de la ligne 3. Quelles sont les variables globales en mémoire ? Quelles sont les variables locales de la fonction actuellement en mémoire ?

visualisation des variables et des espaces des noms

...CORRECTION...

On voit clairement sur l'image :

  • Deux variables globales dans le corps du programme : les deux noms des fonctions, calcul et fois2
  • Trois variables locales à la fonction calcul : a, b et c.

16° Que veut signifier sur Python Tutor le fait que la zone devienne grise une fois qu'une fonction a répondu ?

...CORRECTION...

Le site signale ainsi que l'espace des noms de la fonction n'existe plus. On ne peut donc plus accéder à ces variables.

17° Comment devrait-on nommer mentalement la variable a des lignes 1-2-3. Comment devrait-on nommer mentalement la variable a des lignes 5-6-7 ?

1 2 3 4 5 6 7
def calcul(a, b): a = a + fois2(b) return a def fois2(a): a = a * 2 return a

...CORRECTION...

Lignes 1-2-3 : a_de_la_fonction_calcul par exemple.

Lignes 1-2-3 : a_de_la_fonction_fois2 par exemple.

✎ 18° Fournir le déroulement de l'appel calcul(2, 3), ligne par ligne, pour montrer pourquoi il renvoie 8.

1 2 3 4 5 6 7
def calcul(a, b): a = a + fois2(b) return a def fois2(a): a = a * 2 return a
>>> calcul(2,3) 8

5 - FAQ

Pas de question pour l'instant

Nous avons beaucoup parler ici des variables locales. Dans la prochaine activité, nous verrons les variables globales.

Activité publiée le 26 10 2020
Dernière modification : 29 01 2022
Auteur : ows. h.