Outils pour utilisateurs

Outils du site


cours:informatique:fun_mooc:python3_uca_inria:440_portee_des_variables

Python: Portée des variables

Un *bloc de code* est un ensemble d'instructions contiguës indentées du même nombre de caractères. Lorsque vous faites une opération d'affectation, par exemple x = 1, on dit que vous définissez votre variable x. Ccette notion de définition veut dire qu'une variable référence un objet. Nous avons plusieurs synonymes, que sont:

  • définition,
  • affectation,
  • assignation ou
  • binding.

Ces termes sont utilisés de manière interchangeable. La portée d'une variable détermine de quel endroit du code on peut accéder à cette variable. Python utilise ce qu'on appelle la portée lexicale, ça veut dire que la portée d'une variable est déterminée en fonction de l'endroit dans le code où cette variable est définie.

  • Une variable locale au bloc de code d'une fonction est ce qu'on appelle une variable locale. Une variable que l'on définit dans le bloc de code de la fonction sera locale. Lorsque la fonction retourne, toutes les variables locales de la fonction sont détruites.
  • Une variable définie en dehors de toute fonction est ce que l'on appelle une variable globale. Nous avons donc principalement deux catégories de variables, les variables locales qui sont définies dans le bloc de code des fonctions, et les variables globales qui sont définies en dehors de toute fonction, au niveau du module. Nous abordons ici la notion de portée de variable et en particulier les notions de variables locales et globales.

La règle LEGB

Pour éviter de redéfinir une variable existante ou pour déterminer correctement la valeur d'une variable dans le code on peut s'appuyer sur la règle LEGB qui veut dire:

  • Locale
  • Eenglobante
  • Globale
  • Builtins

Lorsque qu'on référence une variable, on va d'abord chercher si elle a été définie localement à l'endroit où elle est référencée. Donc typiquement, lorsque on référence une variable dans une fonction, on regarde si la variable a été définie localement à la fonction. Si elle n'a pas été définie localement à cette fonction, on va aller la chercher dans les fonctions englobantes. On remonte, de la fonction la plus proche de là où on référence la variable, jusqu'à la fonction la plus externe. Si on ne trouve pas cette variable définie dans les fonctions englobantes, on va la chercher globalement, c'est-à-dire au niveau des variables globales, et pour finir, si je ne la trouve toujours pas, on la cherchera dans le module *builtins*.

Ci-dessous un exemple illustrant la portée des variables:

# définition des 3 variables via tuple unpacking
a, b, c = 1, 1, 1
 
 
def g():
     b, c = 2, 4
     b = b + 10
     def h():
         c = 5
         print(a, b, c)
     h()

Maintenant, essayons de comprendre ce qu'il va se passer lorsque l'on va exécuter ce code. La question principale que l'on a à se poser, c'est: qu'est-ce qu'il va se passer lorsqu'on va faire un print de a, b, c. Quelle variable *a* va être affichée ? Quelle variable *b* va être affichée ? Quelle variable *c* va être affichée ?

Commençons par la variable *a*. On utilise la règle LEGB : locales, fonctions englobantes, globales et ensuite builtins.

  • On regarde si *a* est définie localement à ma fonction *h*. Ce n'est pas le cas.
  • *a* est définie dans les fonctions englobantes ? *g* est une fonction qui englobe *h*, donc *g* est une fonction englobante ; non, *a* n'est pas définie dans la fonction englobante.
  • *a* est définie globalement la variable globale *a* au moment de l'exécution de ce code référence l'entier 1. Donc l'appel de *print* avec la variable *a* va afficher 1.

On procède de la même manière pour les variables b et c:

  • La variable *b* n'est pas définie localement à la fonction *h*.
  • *b* est définie dans la fonctions englobantes *g* et vaut 2, on voit également l'instruction b = b + 10, au moment de l’exécution l'appel de *print* avec *b* va donc afficher 12.

Enfin *c* est définie localement dans *h* et *c* référence l'entier 5. Maintenant, sauvegardons ce code, exécutons-le et vérifions l'évaluation de ce code on voit bien qu'il s'affiche 1, 12 et 5.

>>> g()
1 12 5
 
>>> print(a, b, c)
1 1 1

Pour résumer, une variable définie dans une fonction devient locale à cette fonction, elle peut être vue dans les fonctions qui sont englobées, par contre une variable locale à une fonction ne peut pas être vue à l'extérieur de cette fonction. Une variable globale, au contraire, définie au niveau du module, peut être vue par toutes les fonctions qui sont définies dans ce module.

La règle LEGB évoque au plus haut niveau les builtins. En fait, lorsque on écrit dans l'interpréteur print(a, b, c), on utilise quatre variables. Les paramètres *a*, *b* et *c*, mais également la variable *print*. print est une variable qui référence un objet fonction.

Les fonctions builtins sont disponibles cependant à aucun moment, nous avons importer un module. En fait, toutes les fonctions qui sont définies dans le module builtins sont directement accessibles sans avoir à importer le module builtins. En dernier ressort, si on ne trouve pas un nom de variable, on le cherche dans le module builtins pour pouvoir, si cette variable est définie dans builtins, appeler la fonction correspondante.

Évidemment, on peut importer le module builtins à la main, et inspecter le contenu du module:

import buitins
dir(builtins)

Nous voyons des exceptions, un certain nombre de fonctions que nous avons déjà utilisées, par exemple, la fonction *min*, la fonction *tuple*, la fonction *type*, qui sont des fonctions définies dans le module builtins. Donc à chaque fois qu' on utilise le terme fonction builtins, c'est une fonction qui est définie dans ce module. Seulement, c'est important de comprendre que ces fonctions builtins sont référencées par des variables et qu'une fonction, c'est un objet, et qu'une variable, c'est un nom qui référence un objet, et que par conséquent, on peut redéfinir des fonctions builtins. Regardons cela.

# print référence l'objet fonction builtins.print
>>> print(1)
1
 
# on redéfinit la variable print 
# cette variable est globale et référence un entier
>>> print = 10
 
# cet appel produit une exception, la variable globale print
# est un entier
>>> print(1)
TypeError: 'int' object is not callable
 
 
# refédinition de la variable
>>> print = builtins.print
 
>>> print(1)
1

Si on fait un *print* de 1, je va chercher print dans le module builtins parce que print n'est définie ni localement, ni dans les fonctions englobantes, ni globalement. Par contre, si je fais écrit print = 10, on définit une variable *print* qui référence l'entier 10. Maintenant, *print* est une variable globale qui référence un entier, si on fait un print de 1, on a une exception parce que on ne peut pas appeler un entier 1 pour afficher quelque chose.

Comme la fonction *print* est toujours définie dans le module builtins, on peut toujours écrire récupérer la référence vers le bon objet fonction.

Notion d' espace de nommage

Nous venons de voir la notion de portée de variable mais il y a ici une subtilité importante. Vous avez remarqué qu'il n'y a rien de supérieur à une variable globale, une variable est définie comme globale au niveau d'un module, donc chaque module va définir ses propres variables globales. Cela est rendu possible grâce à un mécanisme d'isolation qui s'appelle espace de nommage. Chaque module va définir son propre espace de nommage, et les variables définies dans l'espace de nommage du module sont ce que l'on appelle des variables globales.

Visibilité des variables de boucle

Une variable de boucle est définie (assignée) dans la boucle et reste visible une fois la boucle terminée. Le plus simple est de le voir sur un exemple:

# La variable 'i' n'est pas définie
try:
    i
except NameError as e:
    print('OOPS', e)
 
# si à présent on fait une boucle
# avec i comme variable de boucle
for i in [0]:
    pass
 
# alors maintenant i est définie
# l'appel suivant ne générera pas d'exception
print(i)

On dit que la variable fuite (en anglais “leak”), dans ce sens qu'elle continue d'exister au delà du bloc de la boucle à proprement parler. On peut être tenté de tirer profit de ce trait, en lisant la valeur de la variable après la boucle. Prudence cependant, car certains points peuvent être sources d'erreur.

Dans l'exemple ci dessous, on exploite se comportement mais sur une boucle vide. Si la boucle ne s'exécute pas du tout, la variable n'est pas affectée et donc elle n'est pas définie.

# une façon très scabreuse de calculer la longueur de l
def length(l):
    for i, x in enumerate(l):
        pass
    return i + 1
 
# l'appel de length sur une liste non vide
# se comporte normalement
length([1, 2, 3])
 
# mais ceci provoque la levée d'une excpetion UnboundLocalError
length([])

Cette levée d'exception est liée à la manière dont python détermine qu'une variable est locale.

Une variable est locale dans une fonction si elle est assignée dans la fonction explicitement (avec une opération d'affectation) ou implicitement (par exemple avec une boucle for comme ici). Mais pour les fonctions, pour une raison d'efficacité, une variable est définie comme locale à la phase de pré-compilation, c'est-à-dire avant l'exécution du code. Le pré-compilateur ne peut pas savoir quel sera l'argument passé à la fonction, il peut simplement savoir qu'il y a une boucle for utilisant la variable *i*, il en conclut que i est locale pour toute la fonction.

Lors du premier appel, on passe une liste à la fonction, liste qui est parcourue par la boucle for. En sortie de boucle, on a bien une variable locale i qui vaut 3. Lors du deuxième appel par contre, on passe une liste vide à la fonction, la boucle for ne peut rien parcourir, donc elle termine immédiatement. Lorsque l'on arrive à la ligne return i + 1 de la fonction, la variable *i* n'a pas de valeur (on doit donc chercher i dans le module), mais *i* a été définie par le pré-compilateur comme étant locale, on a donc dans la même fonction une variable *i* locale et une référence à une variable *i* globale, ce qui provoque l'exception UnboundLocalError.

Pour éviter cette problématique:

  • Déclarer une variable externe à la boucle et à l'affecter à l'intérieur de la boucle;
  • Au minimum initialiser la variable

Les compréhensions

Notez bien que par contre, les variables de compréhension ne fuient pas (contrairement à ce qui se passait en Python 2):

# on détruit la variable i si elle existe
if 'i' in locals(): 
    del i 
 
# en Python 3, les variables de compréhension ne fuitent pas
[i**2 for i in range(3)]
 
# ici i est à nouveau indéfinie
try:
    i
except NameError as e:
    print("OOPS", e)

L' exception UnboundLocalError

Les arguments attendus par la fonction sont considérés comme des variables locales, c'est-à-dire dans l'espace de noms de la fonction. Pour définir une autre variable locale, il suffit de la définir (l'affecter), elle devient alors accessible en lecture :

def ma_fonction1():
    variable1 = "locale"
    print(variable1)
 
>>> ma_fonction1()
locale

Et ceci que l'on ait ou non une variable globale de même nom:

variable2 = "globale"
 
def ma_fonction2():
    variable2 = "locale"
    print(variable2)
 
>>> ma_fonction2()
locale

On peut accéder en lecture à une variable globale sans précaution particulière mais il faut choisir, on ne peut pas utiliser d'abord une variable comme une variable globale, puis essayer de l'affecter localement, ce qui signifie la déclarer comme une locale:

# cet exemple ne fonctionne pas et lève UnboundLocalError
variable4 = "globale"
 
def ma_fonction4():
    # on référence la variable globale
    print(variable4)
    # et maintenant on crée une variable locale
    # cet usage lève une exception UnboundLocalError
    variable4 = "locale"
 
# on "attrape" l'exception
try:
    ma_fonction4()
except Exception as e:
    print(f"OOPS, exception {type(e)}:\n{e}")

L'intérêt de cette erreur est d'interdire de mélanger des variables locales et globales de même nom dans une même fonction. On voit bien que ça serait vite incompréhensible. Donc une variable dans une fonction peut être soit locale si elle est affectée dans la fonction soit globale, mais pas les deux à la fois. Si vous avez une erreur UnboundLocalError, c'est qu'à un moment donné vous avez fait cette confusion.

Pour modifier une variable globale depuis une fonction il faut utiliser l'instruction global:

 Pour résoudre ce conflit il faut explicitement
# déclarer la variable  comme globale
variable5 = "globale"
 
def ma_fonction5():
    global variable5
    # on référence la variable globale
    print("dans la fonction", variable5)
    # cette fois on modifie la variable globale
    variable5 = "changée localement"

Bonnes pratiques

Cela étant dit, l'utilisation de variables globales est généralement considérée comme une mauvaise pratique.

Le fait d'utiliser une variable globale en lecture seule peut rester acceptable, lorsqu'il s'agit de matérialiser une constante qu'il est facile de changer. Mais dans une application aboutie, ces constantes elles-mêmes peuvent être modifiées par l'utilisateur via un système de configuration, donc on préférera passer en argument un objet config.

Et dans les cas où votre code doit recourir à l'utilisation de l'instruction global, c'est très probablement que quelque chose peut être amélioré au niveau de la conception de votre code.

Il est recommandé, au contraire, de passer en argument à une fonction tout le contexte dont elle a besoin pour travailler ; et à l'inverse d'utiliser le résultat d'une fonction plutôt que de modifier une variable globale.

Exemples

Dans quel cas la fonction *f* modifie en place l'objet [1, 2] initialement référencé par la variable var?

Proposition 1

var = [1, 2]
 
def f():
    var = 20
 
f()

La fonction *f* ne modifie pas l'objet puisque la fonction crée une variable locale var distincte de la variable globale var lors de l'affectation.

Proposition 2

var = [1, 2]
 
def f():
    var.append(3)
 
f()

La fonction modifie bien en place l'objet référencé par la variable globale var. En effet, la fonction accède à la variable var qui d'après la règle LEGB est la variable globale. Ensuite, la fonction fait un append sur var, elle modifie donc en place l'objet référencé par la variable globale var.

Proposition 3

var = [1, 2]
 
def f():
    global var
    var = 1
 
f()

La fonction *f* de cette proposition ne modifie pas l'objet référencé. C'est le cas le plus subtil. La fonction déclare la variable var comme globale, donc, dans la fonction, on modifie bien la variable globale var en lui affectant l'objet entier 1, mais on ne modifie pas en place l'objet initialement référencé par var.

Proposition 4

var = [1, 2]
 
def f():
    global var
    var.append(10)
 
f()

La fonction dans la proposition 5 modifie bien en place l'objet référencé. Dans ce cas, l'utilisation de la directive global est inutile, mais légale.

cours/informatique/fun_mooc/python3_uca_inria/440_portee_des_variables.txt · Dernière modification : 2021/05/04 20:34 de 93.28.24.141