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:
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.
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:
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 procède de la même manière pour les variables b et c:
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.
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.
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:
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)
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"
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.
Dans quel cas la fonction *f* modifie en place l'objet [1, 2]
initialement référencé par la variable var
?
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.
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.
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.
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.