Python Portée

Identification

Infoforall

12 - Portée des variables locales


Nous allons voir aujourd'hui une notion un peu difficile car elle demande de comprendre ce qu'est réellement une variable dans Python.

Vous allez voir pourquoi il est inutile de donner des noms différents aux paramètres de deux fonctions s'ils sont censés avoir le même rôle. 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. Voici à quoi ressemblerait alors la Silicon Valley...

Tour de Sauron
Bienvenue au Mordor ! Image CC Public Domain Certification

Il faut avoir vu ou lu le Seigneur des Anneaux pour comprendre la référence...

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.

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

Evaluation ✎ : question 15

Documents de cours : open document ou pdf

Résumé : Version HTML ou fond blanc ou ou PDF (couleur ou gris)

1 - Identifiant des variables

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

01° Sans lancer le programme ci-dessous, répondre aux questions suivantes :

  1. La fonction modification() va-t-elle automatiquement s'exécuter lorsque l'interpréteur va commencer à lire le script en commençant par la ligne 1 ?
  2. Que contient à votre avis la variable a lorsqu'on exécute la ligne 4 ?
  3. Que contient à votre avis la variable a après avoir exécuté la ligne 5 ?
  4. 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 5

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.

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.

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

Imaginons qu'on tape ceci dans la console :

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

Sur l'exemple ci-dessous :

  • Lorsque l'interpréteur tombe sur a, il part voir dans ce tableau nommé "Espace des noms".
  • Il voit que l'identifiant-mémoire associé à la variable a est 68.
  • Il va alors voir le contenu stocké à cet emplacement mémoire 68 et renvoie 5.
espace des noms

03° Dans le menu VIEW, sélectionner allocation mémoire si vous avez la version française ou heap si vous avez la version anglaise de Thonny.

Taper ceci dans la console :

>>> a = 5 >>> b = 7+2

Retrouver le contenu qui est lié à b en suivant la référence.

...CORRECTION...

On voit (sur l'image ci-dessous) que le nom b fait référence à l'adresse-mémoire notée 0xA67BC0.

Dans le tableau Heap, on voit que l'adresse-mémoire 0xA67BC0 contient la valeur  9 .

04° Tapez simplement 30 (et validez avec entrée) dans la console.

>>> 30 30

A-t-on stocké le résultat dans une variable ? Le résultat est-il néanmoins encore en mémoire pour l'instant ?

...CORRECTION...

Aucune variable n'a été créée automatiquement : nous n'avons pas réalisé d'affectation.

Néanmoins, l'interpréteur Python interne à Thonny a gardé ce 30 en mémoire, dans la zone identifiée par 0XA67E60.

05° A l'aide d'une nouvelle affectation a = 50, remplacer le "contenu" de a par 50. L'identifiant-mémoire de a reste-il identique ou a-t-il été modifié ?

...CORRECTION...

La variable a ne fait plus référence à la même zone mémoire.

Ce nom est maintenant associé à la zone identifiée par 0xA680E0.

Cette zone contient 50.

Affectation

Réaliser une nouvelle affectation (avec l'opérateur =) change simplement l'identifiant-mémoire vers laquelle pointe la variable.

Cela pourrait donner ceci sur la représentation fournie quelques lignes ci-dessus :

Notation étrange des identifiants

Les identifiants-mémoires fournis par Thonny sont un peu étranges. Il s'agit de nombres hexadécimaux. Il s'agit de nombres comportant des chiffres supplémentaires après le 9. Ils utilisent A, B, C, D, E et F. Nous verrons ce qu'est l'hexadécimal un peu plus tard pendant l'année.

Pour l'instant, sachez que cela représente juste un nombre.

Si vous voulez exprimer ce nombre en décimal, vous pouvez demander à Python :

>>> 0xA680E0 10911968

Si vous voulez trouver la valeur exprimée en hexadécimal à partir de la valeur en décimal, vous pouvez aussi :

>>> hex(10911968) '0xa680e0'

Bref, c'est juste un nombre. Un affichage dans Thonny directement en décimal, cela nous aurait simplifié la vie mais bon...

Maintenant que vous avez compris qu'une variable Python contient en réalité un identifiant-adresse-mémoire, désactivez la visualisation de l'allocation mémoire (heap dans la version anglaise) dans Thonny. Nous ne l'utiliserons plus, c'était juste pour vous montrer que cette référence-mémoire existe réellement.

L'intérêt d'un langage comme Python est justement de permettre à celui qui programme de ne pas se soucier de la manière dont la mémoire de l'ordinateur est réellement gérée.

Python utilise ce numéro d'identification pour identifier facilement ses variables. S'il existe deux personnes portant le même nom, le numéro de sécurité sociale permet de le distinguer.

Pour connaitre cet identifiant dans Python, on peut utiliser la fonction native id().

06° Désactivez la visualisation de l'allocation-mémoire / heap. En vous inspirant du code ci-dessous, visualiser les identifiants-python de vos variables.

La première version utilisant la fonction native id() fournit la valeur de la référence-mémoire en décimal (avec les chiffres 0123456789).

>>> id(a) ??? >>> id(b) ???

La version ci-dessous utilise en plus la fonction native hex(). Elle donne la même référence-moire mais en l'exprimant en hexadécimal comme dans Thonny (avec les chiffres 0123456789ABCDEF).

>>> hex(id(a)) ??? >>> hex(id(b)) ???
Identifiant Python d'une variable

L'interpréteur Python fait la liaison entre le nom d'une variable et une zone mémoire. Il utilise pour cela une table nommée "Espace des noms".

La fonction native id() fournit le numéro interne à Python qui permet de faire le lien entre une variable et une zone-mémoire.

>>> a = 50 >>> id(a) 99 Valeur imaginaire pour coller au dessin ci-dessus

2 - Espaces des noms

Dans cette partie, nous allons voir qu'il n'existe pas un seul espace des noms mais plusieurs espaces des noms :

  • Le programme "principal" a son espace des noms
  • Chaque fonction a son propre espace des noms

Si vous êtes en classe, faire appel au prof pour qu'il vous détaille l'encadré ci-dessous avant de continuer. Ce sera plus rapide que de lire seul.

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 x.

1 2 3 4 5 6 7 8 9 10 11 12 13
def positif(x): if x < 0: x = -x return x def pas_negatif(x): if x < 0: x = 0 return x x = -5 y = positif(x) z = pas_negatif(x)

L'instruction tabulée sous le if ne sera réalisée que si l'expression derrière le if est évaluée à vraie.

Les lignes 2 et 3 signifient : si x est inférieur à 0 alors on effectue la ligne 3 (qui inverse le signe de x).

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 x présentes dans la fonction positif() pourraient être renommées mentalement x1 pour indiquer qu'il s'agit des variables x de la première fonction
  2. Les variables x présentes dans la fonction pas_negatif() pourraient être renommées mentalement x2 pour indiquer qu'il s'agit des variables x de la deuxième fonction
  3. Les variables x présentes dans le corps du programme lui-même pourraient être renommées mentalement xpp pour indiquer qu'il s'agit des variables x du programme principal
1 2 3 4 5 6 7 8 9 10 11 12 13
def positif(x1): if x1 < 0: x1 = -x1 return x1 def pas_negatif(x2): if x2 < 0: x2 = 0 return x2 xpp = -5 y = positif(xpp) z = pas_negatif(xpp)

Si vous rajouter mentalement cela, vous verrez que cela simplifie grandement la lecture de votre programme. Plus de confusion possibile entre les différents x.

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

>>> x -5 >>> y 5 >>> z 0

07° Exécuter mentalement les lignes 11 et 12.

1 2 3 4 5 6 7 8 9 10 11 12 13
def positif(x1): if x1 < 0: x1 = -x1 return x1 def pas_negatif(x2): if x2 < 0: x2 = 0 return x2 xpp = -5 y = positif(xpp) z = pas_negatif(xpp)
  1. Quelle est l'argument envoyé à la fonction positif() ?
  2. Dans quel paramètre stocke-t-on cette valeur ?
  3. Que contient la variable xpp après exécution ?
  4. Que contient la variable y après exécution ?

...CORRECTION...

Ligne 11 : xpp = -5.

Ligne 12 : cela revient donc à faire y = positif(-5)

L'argument envoyé est donc -5.

On stocke cet argument dans le paramètre x1.

Lors de cet appel, on affecte donc -5 à x1.

Comme cette valeur est négative, on affecte (en ligne 3) x1 à la valeur +5.

On renvoie ce +5 et on voit, ligne 12, qu'on l'affecte à la variable y.

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

08° Exécuter mentalement la ligne 13.

  1. Quel est l'argument envoyé à la fonction pas_negatif() ?
  2. Dans quel paramètre stocke-t-on cet argument ?
  3. Que contient la variable xpp 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
def positif(x1): if x1 < 0: x1 = -x1 return x1 def pas_negatif(x2): if x2 < 0: x2 = 0 return x2 xpp = -5 y = positif(xpp) z = pas_negatif(xpp)

...CORRECTION...

L'argument envoyé est donc encore -5.

On stocke cet argument dans le paramètre x2.

Lors de cet appel, on affecte donc -5 à x2.

Comme cette valeur est négative, on affecte (en ligne 8) 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'espace des noms ?

Notamment pour vous évitez de lire toutes les lignes du code dès que vous voulez créer une fonction. Si les espaces des noms n'existaient pas, il faudrait en effet vérifier que le nom x ne soit pas déjà utilisé. Finalement, l'espace des noms, c'est pas mal non ?

Autre raison : lorsqu'on sort d'une fonction, l'espace des noms de cette fonction disparaît et on libère ainsi de la mémoire.

La suite de l'activité consiste simplement en quelques manipulations qui vont vous permettre de bien cerner pourquoi et comment cela fonctionne. Mais en compétences attendues, il suffit d'avoir compris le résumé ci-dessus.

3 - fString

f-Strings

Les f-Strings sont des strings permettant d'insérer facilement le contenu d'une variable dans un string sinon tout à fait normal.

Pour créer un f-Strings, il suffit de ... placer f devant les guillemets d'ouverture de votre string.

On peut alors noter des noms de variables entre accolades {...} et lors de la création du string, l'interpéteur Python remplacer les accolades par le contenu de la variable.

1 2 3
def exemple_f(): a = 5 return f"La variable a contient {a}"

Le résultat dans la console :

>>> exemple_f() "La variable a contient 5"

Nous allons maintenant étudiée deux fontions qui ont une particularité : leurs paramètres portent le même nom.

1 2 3 4 5 6
def cercle_bleu(rayon, largeur): '''Trace un cercle bleu''' def cercle_rouge(rayon, largeur): '''Trace un cercle rouge'''

09° Mettre ces fonctions en mémoire.

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 26 27 28 29
import turtle as trt def cercle_bleu(rayon, largeur): '''Trace un cercle bleu :: param rayon(int) :: le rayon du cercle :: param largeur(int) :: largeur du crayon :: return (str) :: un string contenant les id des paramètres ''' feutre = trt.Turtle() feutre.color("blue") feutre.pensize(largeur) feutre.circle(rayon, 360) return f"feutre id {id(feutre)} -- rayon id {id(rayon)} -- largeur id {id(largeur)}" def cercle_rouge(rayon, largeur): '''Trace un cercle rouge :: param rayon(int) :: le rayon du cercle :: param largeur(int) :: largeur du crayon :: return (str) :: un string contenant les id des paramètres ''' feutre = trt.Turtle() feutre.color("red") feutre.pensize(largeur) feutre.circle(rayon, 360) return f"feutre id {id(feutre)} -- rayon id {id(rayon)} -- largeur id {id(largeur)}"

Utilisez ensuite la console pour activer les fonctions.

>>> cercle_bleu(50, 5) >>> cercle_rouge(100, 10)

Question

Les paramètres nommés rayon et largeur font-ils référence à la même zone mémoire ?

Idem avec la variable locale feutre ?

...CORRECTION...

>>> cercle_bleu(50, 5) 'feutre id 139938418991792 -- rayon id 9201408 -- largeur id 9199968' >>> cercle_rouge(100, 10) 'feutre id 139938418993080 -- rayon id 9203008 -- largeur id 9200128'

On constate bien que ni feutre, ni rayon, ni largeur n'ont le même identifiant lors des deux appels. Cela veut bien dire qu'il s'agit de variables portant le même nom mais qui ne font pas référence à la même zone mémoire.

Il y a ainsi

  • rayonbleu et rayonrouge.
  • largeurbleu et largeurrouge.
  • feutrebleu et feutrerouge.
Espace des noms des fonctions

Chaque fonction possède son propre espace des noms et cela va même plus loin en réalité : lorsqu'on fait appel deux fois à une fonction, on a en réalité un espace des noms pour chacune des fonctions. Cela veut dire que l'espace des noms et les variables qui en font partie ne seront pas les mêmes lors des deux appels.

10° Effectuer les tâches suivantes :

  1. Mettre cette fonction en mémoire. On voit qu'elle crée une variable texte faisant référence au string "J'existe !" si le paramètre reçu est positif.
  2. 1 2 3 4
    def espace(n): if n > 0: texte = "J'existe !" return texte
  3. Lancer une première fois cette fonction en fournissant 12 par exemple. On voit qu'elle crée bien la variable locale texte et renvoie son contenu.
  4. >>> espace(10) "J'existe !"
  5. Expliquer pourquoi lors du deuxième appel avec une valeur négative, on obtient une erreur :
  6. >>> espace(-45) UnboundLocalError: local variable 'texte' referenced before assignment
  7. Conclusion : Que se passe-t-il pour les variables locales lors de la sortie d'une fonction : elles sont détruites ou elles sont stockées en attendant un autre appel ?

...CORRECTION...

On voit bien que lors du deuxième appel, la variable texte_appel_2 n'existe pas. En effet, la variable texte_appel_1 n'existait que lors du premier appel. Lorsqu'on demande de renvoyer la deuxième fois cette variable, il y a une erreur puisque le paramètre reçu est négatif et n'a pas permis de créer la variable AVANT de la renvoyer. Le fait qu'elle avait été créé lors du premier appel n'a donc aucune importante : il s'agit d'une nouvelle exécution et on n'a plus accès à l'ancien espace des noms même s'il s'agit de la même fonction.

L'espace des noms d'une fonction et les variables locales qu'il contient sont donc détruits dès que la fonction répond.

Le mécanisme d'espace des noms permet d'écrire des programmes plus simples car on n'a pas besoin de créer des noms de variables différents dans les différentes fonctions. Un lecteur du code doit juste comprendre qu'il ne s'agit PAS des mêmes variables.

4 - Portée des variables locales

Nous allons maintenant tenter de voir si on peut voir le contenu des variables des fonctions de n'importe où dans le programme.

11° Placer ces fonctions en mémoire. Utiliser ensuite les instructions dans la console.

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
>>> calcul(2,3) 8 >>> c NameError: name 'c' is not defined

Question : les variables locales existent-elles en dehors de l'exécution de la fonction ?

...CORRECTION...

Les variables déclarées dans les fonctions 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.

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.

12° Partir sur 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/desctruction des variables au cours de l'exécution du programme.

13° 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.

14° Que veut signifier 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.

Bilan : portée des variables locales en Python

Si on résume la portée des variables locales en image, ça donne ceci :

portee-variable-locale

Vous devez savoir 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.

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

La question finale ci-dessous n'a pour but que de vous faire réfléchir. Les deux fonctions qu'on y trouve ne servent à rien, sauf à vérifier que vous parvenez bien à raisonner sur les espaces des noms.

✎ 15° Expliquer le plus clairement possible le contenu final de reponse. Le mieux est de faire cela étape par étape.

1 2 3 4 5 6 7 8 9 10
def question_finale(x, y): a = x + 10*y b = calcul_bizarre(a) return a def calcul_bizarre(z): a = 2 return z*a reponse = question_finale(4,10)

5 - FAQ

Pas de question pour l'instant

Il faut donc à partir de maintenant vous souvenir de l'existence de l'espace des noms du programme et des fonctions.

N'oubliez pas que l'espace des noms des fonctions n'est accessible que depuis la fonction et qu'il est détruit lorsqu'on sort de la fonction.

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