Python Portée

Identification

Infoforall

9 - Portée des variables


Nous allons voir aujourd'hui une notion un peu difficile car elle demande de comprendre ce qu'est réellement une variable dans Python (et dans la plupart des autres langages).

Vous allez par exemple 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. Il n'y a aucune confusion possible.

1 2 3 4 5 6
def nouveau_stylo(ecriture, fond, largeur) : def deplacer(feutre, x, y) : def trois(feutre, distance, angle) : def triangle(feutre, cote) : def trace_triangle(ecriture, fond, largeur, x, y, cote) :

Cette version est plus facile que de devoir donner des noms proches mais différents comme :

1 2 3 4 5 6
def nouveau_stylo(ecriture, fond, largeur) : def deplacer(feutre, x, y) : def trois(ftr, distance, angle) : def triangle(f, cote) : def trace_triangle(ecrit, fond, largeur, x, y, cote) :

Vous imaginez si on devait se souvenir 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

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. Voyons un peu ces qu'est une variable dans le langage Python.

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

Evaluation ✎ : question 15

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 et pratique mais il ne permet pas d'expliquer certains cas plus complexes que nous pourrions rencontrer avec Python. Regardons un de ces cas particulier.

01° Sans lancer le code 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érer.

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 est en réalité un alias ou raccourci pour atteindre une zone mémoire sans avoir à connaître et fournir son adresse réelle.

L'interpréteur se crée un tableau faisant la liaison entre le nom que vous avez utilisé et une zone de la mémoire (adr dans les images ci-dessous).

Imaginons qu'on tape ceci dans la console :

>>> a = 68 >>> b = 85 >>> a a

Sur l'exemple ci-dessous :

  • Lorsque l'interpréteur tombe sur a, il part voir dans ce tableau.
  • Il voit que l'identifiant-mémoire associé à cette variable 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 >>> 30 30

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. 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° Remplacer le contenu de a par 50 à l'aide de  a = 50 . Que constatez-vous sur l'identifiant de a ? Reste-il identique ou a-t-il été modifié ?

...CORRECTION...

La variable qui porte maintenant le nom 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.

Comme on peut le constater, réaliser une nouvelle affectation (avec le signe  = ) change simplement l'identifiant associé à la variable et donc la zone vers laquelle pointe la variable.

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

Notation étrange des identifiants

Vous avez certainement remarqué que les identifiants 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. On peut ainsi utiliser 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. S'ils l'avaient affiché dans Thonny directement en décimal, cela nous aurait simplifié la vie mais bon...

Maintenant que vous avez compris qu'une variable contient en réalité un identifiant-adresse-mémoire, désactivez la visualisation de l'allocation mémoire / heap dans Thonny.

Python utilise ce numéro d'identification pour identifier facilement ses variables. Cet identifiant permet de les différencier à coup sur. S'il existe deux personnes portant le même nom, on fait pareil avec le numéro de sécurité sociale. Notez bien que dans l'espace des noms, on ne peut pas stocker en même temps deux fois le même nom.

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 correspond à la valeur en décimal, la seconde valeur à la même valeur mais exprimée en hexadécimal.

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

L'interpréteur Python fait la liaison entre le code fourni et la mémoire.

La fonction id fournit un numéro interne à Python qui permet de faire le lien entre une variable et un contenu mémoire.

Avec Python, il ne s'agit juste d'un simple identifiant, pas de la vraie adresse-mémoire. Du point de vue utilisateur, aucune différence.

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

Portée des variables et espaces des noms

Les variables créées dans les fonctions ne sont lisibles et modifiables qu'à l'intérieur de cette fonction.

On dit qu'elles sont des variables locales à la fonction.

Exemple : nous avons ici plusieus 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)

Visiblement, le programme contient beaucoup de choses nommées 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 :

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

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

  1. Quel est l'argument envoyé à la fonction positif ?
  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 y 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...

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 d'avoir à aller lire toutes les lignes de code de votre projet 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é par quelqu'un. Finalement, l'espace des noms, c'est pas mal non ?

Autre raison : lorsqu'on sort d'une fonction, l'espace des noms de la fonction disparaît. Inutile de continuer à référencer des variables qui ne sont plus utilisées.

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

On trouve parfois un f devant les strings précédents.

Cela les transforme en f-Strings.

Avec un tel string, on peut noter des noms de variables entre des 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 le Shell :

>>> exemple_f() La variable a contient 5

Du coup, nous pourrions utiliser de tels fStrings pour facilement renvoyer les identifiants-mémoires des paramètres de deux fonctions. La particularité : les paramètres portent le même nom.

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)}"

09° Mettre ces fonctions en mémoire. Utilisez ensuite la console pour activer les consoles.

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

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 id139938418991792 -- rayon id9201408 --largeur id 9199968' >>> cercle_rouge(100, 10) 'feutre id139938418993080 -- rayon id9203008 --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'ils s'agit de variables portant le même nom mais qui ne vont pas référence à la même zone mémoire.

Il y a ainsi feutrebleu et feutrerouge.

10° Si on relance la fonction cercle_rouge, on constate que les références des paramètres vont toujours référence aux mêmes identifiants mémoires. C'est normal, on envoie encore 100 et 10.

Par contre, que constate-t-on pour la variable feutre ?

Que devient d'ailleurs cette variable lorsqu'on sort de la fonction ?

>>> cercle_bleu(50, 5) 'feutre id139938418991792 -- rayon id9201408 --largeur id 9199968' >>> cercle_rouge(100, 10) 'feutre id139938418993080 -- rayon id9203008 --largeur id 9200128' >>> cercle_rouge(100, 10) 'feutre id139938418993472 -- rayon id9203008 --largeur id 9200128'

...CORRECTION...

On constate que la variable locale ne fait pas toujours référence à la même zone mémoire à chaque appel du code. C'est normal : pourqoi faudrait-il nécéssairement stocker les choses au même endroit.

Surtout qu'à la fin, tout disparait : les variables locales disparaissent avec l'espace des noms de la fonction puisque c'est le seul endroit où se trouve la table associant alias et identifiant.

Comme vous devriez le comprendre maintenant, le mécanisme d'espace des noms permet d'écrire au final des codes plus simples car on n'a pas besoin de créer des noms de variables différents dans les différentes fonctions alors qu'elles servent à la même chose. Par contre, cela nécessite que le lecteur du code comprenne bien 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° Utiliser les lignes suivantes dans la console après avoir placé ces fonctions en mémoire.

Question : les variables locales internes aux fonctions existent-elles en dehors de l'exécution de la fonction ? Autrement dit, peut-on accéder à une variable locale à une fonction depuis l'extérieur ?

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

...CORRECTION...

Les variables déclarées dans les fonctions ne sont lisibles que dans le 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.

Portée des variables locales

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

portee-variable-locale

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.

✎ 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 11 2020
Auteur : ows. h.