IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Python, de zéro


précédentsommairesuivant

XII. La portée et la visibilité des variables

XII-1. La portée

Avec l’avènement des langages structurés s’est posé pour les variables le problème de la portée (partie du code où une variable garde sa valeur) et de la visibilité (partie du code où une variable est accessible).

Pour bien comprendre comment Python résout ces problèmes, il faut d’abord comprendre la notion de « scope ».

Le « scope » est la zone de visibilité d’un code Python. Python ne connait que 2 scopes :

  • le scope dit global, qui contient la globalité du code Python (tout le code source),
  • le scope dit local, qui ne concerne que les parties de codes situées dans une fonction ou un objet (sera vu ultérieurement). Bien évidemment, s’il n’y a qu’un seul scope global, il y a autant de scope locaux différents qu’il y a de fonctions ou objets distincts.

XII-2. La visibilité

XII-2-a. Principes

Une variable définie dans un scope est alors visible et accessible depuis tout ce scope. Cela signifie donc qu’une variable définie dans le scope global est alors visible de tout le code Python y compris dans tout scope local c’est-à-dire y compris dans toute fonction ou objet.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
>>> def fct(): print(toto)      # Affichage d’une variable du scope global
...
>>> toto=123                    # Création de la variable dans le scope global
>>> print(toto)                 # Vérification de son contenu
123
>>> fct()                       # Appel de la fonction qui affiche la variable
123

De même, une variable définie dans le scope local d’une fonction est alors accessible depuis tout le code de la fonction ; y compris dans une sous‑fonction éventuelle.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
>>> def fct():                  # Création d'une fonction et de son scope local
...    def xxx(): print(toto)   # Affichage d’une variable du scope local de la fonction
...    toto=200                 # Création de la variable dans le scope local de la fonction
...    xxx()                    # Appel de la sous-fonction qui affiche la variable
...
>>> fct()                       # Appel de la fonction
200

Toutefois, il faut que l’instruction de création soit quand même atteinte pour que la variable soit créée.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
>>> if True: titi=123          # Comme le test est vrai, la variable "titi" est créée

...
>>> if False: tata=456         # Comme le test est faux, la variable "tata" n’est pas créée

...
>>> print(titi)
123
>>> print(tata)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'tata' is not defined

Redéfinir dans le scope local une variable déjà existante dans le scope global ne gêne pas. Dans ce cas, la variable située dans le scope local prendra le pas sur la variable du scope global ; laquelle redeviendra accessible sitôt le scope local terminé.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
>>> def fct():                 # Création d'une fonction et de son scope local
...    toto=200                # Création d’une variable locale qui masque la globale
...    print(toto)             # Affichage de la variable locale
...
>>> toto=100                   # Variable globale
>>> print(toto)                # Vérification de son contenu
100
>>> fct()                      # Appel de la fonction qui utilise sa propre variable
200
>>> print(toto)                # Fonction terminée, la variable globale est redevenue accessible
100

Par défaut, au sein d’un scope local, une variable provenant du scope global n’est accessible qu’en lecture seule…

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
>>> def fct():                 # Création d'une fonction et de son scope local
...    print(toto)             # Affichage de la variable globale
...    toto=200                # Tentative de modifier la variable globale => erreur
...
>>> toto=100
>>> fct()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in fct
UnboundLocalError: local variable 'toto' referenced before assignment

… à moins qu’on ne spécifie explicitement avec l'instruction global que l’on veut pouvoir accéder pleinement à la variable située dans le scope global.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
>>> def fct():                 # Création d'une fonction et de son scope local
...    global toto             # Demande d'accès complet à la variable globale
...    print(toto)             # Affichage de la variable globale
...    toto=200                # Modification de la variable globale
...
>>> toto=100
>>> fct()
100
>>> print(toto)
200

Attention, recourir de façon continue à cette technique peut causer plus de soucis que ce que ça n’en résout. La majorité des programmeurs professionnels évitent donc l'utilisation des variables globales qui cassent l'indépendance d'une fonction vis-à-vis de son environnement (si la variable globale change de forme ou de nom, il faut alors passer en revue tous les endroits où elle est utilisée) et qui ne sont pas contrôlées (si la valeur d'une globale change par erreur, difficile alors de trouver l'endroit du changement).

Par ailleurs, il n’y a pas de notion de « hiérarchie » au sein des scopes locaux. Faire appel au mot clef global dans une sous‑fonction fera référence au seul et unique scope global du programme et non au scope de la fonction supérieure (qui, dans une autre logique, aurait pu être vu comme « global » dans la fonction).

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
>>> def fct():                        # Création d'une fonction et de son scope local
...    def xxx():                     # Création d'une sous-fonction qui utilise le même scope
...        global toto                # Référence au seul et unique scope global
...        toto=300                   # Modification de la variable globale
...        print("xxx:", toto)        # Affichage de la variable globale
...    toto=200                       # Variable locale à la fonction
...    xxx()                          # Appel de la fonction qui modifie la globale
...    print("fct:", toto)            # Affichage de la variable locale
...
>>> toto=100
>>> print(toto)
100
>>> fct()
xxx: 300
fct: 200
>>> print(toto)
300

Attention, toute référence à une variable du scope global interdit ensuite sa redéfinition dans le scope local.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
>>> def fct():                        # Création d'une fonction et de son scope local
...    print(toto)                    # Affichage de la variable du scope global
...    toto=200                       # Tentative de création d'une variable locale => erreur
...
>>> toto=100
>>> fct()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in fct
UnboundLocalError: local variable 'toto' referenced before assignment

En fait, avec ce genre de syntaxe, Python croit qu'on tente de modifier la variable du scope global sans l’avoir demandé avec l'instruction global.

Le contenu complet des scopes local et global peut se récupérer au travers des fonctions locals() et globals(). Ces deux fonctions renvoient les dictionnaires associés à ces deux scopes ayant les variables pour clefs et leurs valeurs comme valeur…

 
Sélectionnez
1.
2.
3.
4.
5.
>>> toto=300
>>> globals()["toto"]
300
>>> locals()["toto"]
300

… et ces dictionnaires peuvent aussi être modifiés ce qui se répercute sur les variables par ricochet.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
>>> toto=300
>>> toto
300
>>> locals()["toto"]=400
>>> toto
400

XII-2-b. Conséquences

Cette notion de scope entraine beaucoup de conséquences. La première c’est que contrairement à d’autres langages (comme le C), une variable n’est pas limitée à son bloc de travail, mais existe dans tout le scope dudit bloc.

Et donc, dans la séquence suivante…

 
Sélectionnez
1.
2.
3.
4.
>>> for i in range(10): pass
...
>>> print(i)
9

… la variable i existe même en dehors de la boucle. C’est d’ailleurs ce qui permet à ce genre d’instructions…

 
Sélectionnez
1.
2.
3.
4.
5.
6.
for i in range(10):
    if(test quelconque vrai)…: trouve=True
else:
    trouve=False

print(trouve)

… de fonctionner correctement.

De plus, cela peut amener des effets de bord difficilement compréhensibles, surtout quand on définit des fonctions utilisant des variables venues du scope global.

Prenons par exemple un tableau de fonctions qui est créé au fur et à mesure ; où chaque fonction créée utilise dans son traitement la variable de boucle du scope global :

 
Sélectionnez
1.
2.
3.
4.
5.
>>> tabF=[]
>>> for i in range(3):
...    def my_func(x):
...        return x+i
...    tabF.append(my_func)

Comme on le voit, le tableau contient différentes fonctions qui sont censées ajouter chacune l'indice de leur rang au paramètre reçu. Et donc tabF[0] ajoutera 0 à son paramètre, tabF[1] ajoutera 1 etc.

Ainsi, si on demande à toutes les exécuter avec le paramètre 50, on s’attend à voir s’afficher des nombres croissants à partir de ce nombre (50, 51, 52, etc.).

Or, au résultat, ce n’est pas du tout ce qui se passe…

 
Sélectionnez
1.
2.
3.
4.
5.
>>> for f in tabF:
...    print(f, f(50))
<function my_func at 0x7f57fc4f16e0>, 52
<function my_func at 0x7f57fc4f1758>, 52
<function my_func at 0x7f57fc4f17d0>, 52

L’explication est que chaque fonction additionne au paramètre reçu la valeur de i à son état actuel (tel qu'il est en fin de boucle range(3)) et non la valeur de i au moment où la fonction a été créée. On peut s’en rendre compte plus aisément si on rajoute par exemple l’instruction i=123 avant de faire le test (on aura 125, 125, 125).

Pour s’en sortir, il est impératif de ne pas utiliser de valeur extérieure à la fonction, mais un paramètre local, même si ce paramètre se contente de récupérer la variable en cours d’itération…

 
Sélectionnez
1.
2.
3.
4.
5.
>>> tabF=[]
>>> for i in range(3):
...    def my_func(x, y=i):
...        return x+y
...    tabF.append(my_func)

… ce qui donne ainsi le résultat attendu.

 
Sélectionnez
1.
2.
3.
4.
5.
>>> for f in tabF:
...    print(f, f(50))
<function my_func at 0x7f57fc4f16e0>, 50
<function my_func at 0x7f57fc4f1758>, 51
<function my_func at 0x7f57fc4f17d0>, 52

Et pour finir, rien n’interdit d’utiliser en interne le même nom de paramètre que celui de la variable en cours d’itération ; et de remplacer la définition de la fonction par une lambda.

 
Sélectionnez
1.
2.
3.
>>> tabF=[]
>>> for i in range(3):
...    tabF.append(lambda x, i=i: x+i)

XII-3. La portée

À l'instar d’autres langages (comme le static du C), Python permet de pérenniser la valeur d'une variable en dehors de son scope au travers du mécanisme d'un argument par défaut d'une fonction positionné sur un objet mutable (liste, dictionnaire).

En effet, l'objet par défaut étant constitué lors de la création de la fonction et non à son appel, il conserve sa valeur entre chaque appel (cf. chapitre sur les paramètres par défauts d'une fonctionValeur par défaut des paramètres).

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
>>> def fct(save=[]):
...    toto=save.pop() if save else 0
...    toto+=1
...    print(toto)
...    save.append(toto)

>>> fct()
1
>>> fct()
2
>>> fct()
3

Remarque : on peut tout de même se demander alors l'utilité de la variable toto dans ce cas…

XII-4. Identité et hash d’une variable

XII-4-a. Identité d’une variable

Toute variable possède une identité. Il s’agit d’un entier garantissant l’unicité de la variable pour toute sa durée de vie. Si l’on veut essayer de faire une analogie sommaire avec le C, on peut, dans une certaine mesure, parler alors d’adresse.

La récupération de cette identité peut se faire par la fonction id().

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
>>> a=500
>>> b=600
>>> c=3.14
>>> id(a)
140047619882064
>>> id(b)
140047620423600
>>> id(c)
140047620461072

Il est intéressant de noter que toute variable créée par copie aura alors le même identifiant que la variable d'origine. Et ce, jusqu'à ce qu'elle subisse une nouvelle affectation (même identique).

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
>>> a=3.14
>>> b=a
>>> id(a)
140047620461144
>>> id(b)
140047620461144
>>> b=3.14
>>> id(b)
140047620461072

La fonction id() peut aussi être appliquée à des constantes pures, mais les résultats de cette opération (qui changent avec l'évolution du code) ne veulent rien dire de significatif.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
>>> id(500)
140047619882064
>>> id(600)
140047619882064
>>> id(700)
140047619882064
>>> a=5
>>> id(700)
140047619881008

Et enfin, les valeurs comprises entre 5 et 256 sont stockées « en dur » dans le moteur Python. Toute variable ayant l’une de ces valeurs aura alors toujours le même identifiant.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
>>> a=5
>>> b=5
>>> id(a)
10771648
>>> id(b)
10771648
>>> b=6
>>> c=6
>>> id(b)
10771680
>>> id(c)
10771680
>>> id(6)
10771680

Remarque : si la valeur 256 peut se comprendre de par sa signification bien connue de tous les développeurs, en revanche, en ce qui concerne la raison des valeurs négatives allant jusqu'à 5nous n’avons pas d’explication.

XII-4-b. Hash d’une variable

En plus de son identité, toute variable possède un « hash » c’est-à-dire une méthode calculant son empreinte numérique. Tout comme pour l’identité, il s’agit aussi d’un entier permettant de comparer rapidement les choses (si deux hashs sont égaux alors les valeurs dont ils sont issus le sont aussi).

La récupération de ce hash peut se faire par la fonction hash().

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
>>> a="toto"
>>> b="titi"
>>> c=tuple((1, 2, 3))
>>> hash(a)
-6444533920638468822
>>> hash(b)
4818377268154382741
>>> hash(c)
2528502973977326415

Remarque : la fonction hash() ne peut s’appliquer qu’aux types immuables. Les listes, ensembles et dictionnaires en sont donc exclus. En fait, une valeur ne peut servir de clef à un dictionnaire que si elle est « hashable ».

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
>>> hash(list())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> d={set() : "xxx"}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'set'

XII-5. De la bonne façon de copier les choses

Le paragraphe précédent permet de mieux comprendre le mécanisme d’affectation de variables en Python et ce qui en découle. Quand on écrit (par exemple) a=345 puis b=a, Python dépose un nombre 345 dans une case mémoire puis lui assigne une étiquette nommée a et une autre nommée b. C'est la raison pour laquelle id(a) et id(b) affichent la même chose.

Remarque : si ensuite on écrit b=345, Python dépose 345 dans une seconde case et lui réassigne l’étiquette b. Il ne va évidemment pas se fatiguer à vérifier si 345 a déjà été déposé quelque part, ce serait une perte de temps avec trop peu de plus‑value (à l'exception des nombres entre -5 et 256 dont on a déjà parlé au paragraphe précédent).

Alors ce mécanisme d'affectation de différentes étiquettes à une même case mémoire ne pose absolument aucun souci quand il s'agit de types immuables (int, float, str, tuple). Pour ces types, écrire a=345 puis b=a n'empêchera pas d'écrire ensuite a=678. Python se comportera parfaitement à propos de a et de b qui auront leurs valeurs adéquates et personnelles.

Là où les soucis arrivent, c'est quand il s'agit de types mutables (listes, ensembles, dictionnaires et leurs dérivés). En effet, qui dit « type mutable » dit « modification de son intégrité ». Or, tout comme pour les types simples, le type mutable n'est créé qu'une seule fois en mémoire et ensuite, les diverses variables viennent s'y adresser.

Ainsi, écrire a=[1, 2, 3] puis b=a fait pointer ces deux variables vers la même zone mémoire. Et si cette zone est modifiée, la modification se répercute sur les deux variables.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
>>> a=[1, 2, 3]
>>> b=a
>>> a.append("toto")
>>> print(a, id(a))
([1, 2, 3, 'toto'], 139969669202184)
>>> print(b, id(b))
([1, 2, 3, 'toto'], 139969669202184)

Cela ne veut pas dire qu'il est interdit d'écrire b=a, juste qu'il faut savoir ce que cela entraine. C'est un peu le même souci qu'avec les paramètres par défaut des fonctions quand on veut y mettre un type mutable (cf. chapitre sur les fonctions et les valeurs par défaut des argumentsValeur par défaut des paramètres).

Néanmoins, il est possible, si nécessaire, de créer une vraie copie d'un type mutable, copie ayant alors ensuite sa vie propre. Si par exemple il s'agit de listes, il suffit d'utiliser le slice, la fonction list() ou plus récemment la méthode copy() pour créer cette copie.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
>>> a=[1, 2, 3]
>>> b=a[:]
>>> c=list(a)
>>> d=a.copy()
>>> a.append("pim")
>>> b.append("pam")
>>> c.append("poum")
>>> d.append("toto")
>>> print(a, id(a))
([1, 2, 3, 'pim'], 139977338712392)
>>> print(b, id(b))
([1, 2, 3, 'pam'], 139977339206280)
>>> print(c, id(c))
([1, 2, 3, 'poum'], 139977338007624)
>>> print(d, id(d))
([1, 2, 3, 'toto'], 139977337915272)

Et pour les autres types (ensembles, dictionnaires), s'il n'y a pas le slice, il reste quand même leurs fonctions dédiées (set() et dict()) et aussi leurs propres méthodes copy().

Attention : ces méthodes ne fonctionnent qu'à condition que les éléments de premier niveau soient eux-mêmes immuables. Cela fonctionnera donc parfaitement pour une liste d'entiers où tous les entiers de la liste d'origine seront recopiés dans la liste de destination ; mais cela ne fonctionnera pas pour une liste de listes, car là, seules les références des sous-listes composant la liste de base seront copiées dans la liste de destination. Dans ce dernier cas, le problème se reposera si on tente ensuite de donner une vie différente à l'une des sous-listes.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
>>> a=[[1, 2, 3], [4, 5, 6]]
>>> b=a.copy()
>>> a.append("Pim")
>>> b.append("Pam")
>>> print(a)
[[1, 2, 3], [4, 5, 6], 'Pim']
>>> print(b)
[[1, 2, 3], [4, 5, 6], 'Pam']
# Jusque là tout est ok

# Tentons maintenant de modifier une sous-liste de la première variable…
>>> a[0][0]="Poum"
>>> print(a)
[['Poum', 2, 3], [4, 5, 6], 'Pim']
>>> print(b)
[['Poum', 2, 3], [4, 5, 6], 'Pam']

Pour régler ce type de problèmes, il faut partir dans des opérations à base de récursivité. Il existe toutefois un module « copy » (les modules seront vus ultérieurement) qui contient des outils permettant ce travail de copie indépendante et complète.

Rappel : tous ces soucis concernant la modification d’un original qui peut se répercuter sur sa copie n'existent pas avec une chaîne ou un tuple qui ne sont pas modifiables (immuable).


précédentsommairesuivant

Copyright © 2022 Svear (svear@free.fr) Permission est accordée de copier, distribuer ou modifier ce document selon les termes de la « Licence de Documentation Libre GNU » (GNU Free Documentation License), version 1.1 ou toute version ultérieure publiée par la Free Software Foundation.