Chapitre 20 : La Portée des Variables (Scope)
LEGB, global, nonlocal
Objectif
Ce module a pour but de vous faire comprendre le concept de portée des variables (scope) en Python. Vous apprendrez comment Python détermine où chercher une variable et la règle LEGB (Local, Enclosing, Global, Built-in) qui régit ce comportement.
1. Qu'est-ce que la Portée (Scope) ?
La portée d'une variable est la partie du code où cette variable est accessible. Python ne permet pas d'accéder à n'importe quelle variable depuis n'importe où.
def ma_fonction():
# 'x' est une variable locale à ma_fonction
x = 10
print(x)
ma_fonction() # Affiche 10
# Essayer d'accéder à 'x' ici lèvera une NameError
# car 'x' n'existe que dans le scope de ma_fonction.
# print(x) # NameError: name 'x' is not defined
2. La Règle LEGB
Python utilise la règle LEGB pour résoudre les noms de variables. Quand vous utilisez une variable, Python la cherche dans l'ordre suivant :
- L - Local : La portée la plus interne, qui contient les noms définis à l'intérieur de la fonction actuelle (et qui ne sont pas déclarés
globalounonlocal). - E - Enclosing (Englobante) : La portée des fonctions englobantes (closures). Si une fonction est définie à l'intérieur d'une autre fonction, la fonction interne peut accéder aux variables de la fonction externe.
- G - Global : La portée au niveau du module. Ce sont les noms définis au plus haut niveau d'un script ou d'un module.
- B - Built-in : La portée la plus externe, qui contient les noms prédéfinis en Python (
print,len,str, etc.).
Python s'arrête à la première occurrence qu'il trouve.
Exemple illustrant la règle LEGB
# B - Built-in (print est une fonction built-in)
# G - Global
x_global = "Je suis globale"
def fonction_externe():
# E - Enclosing
x_enclosing = "Je suis dans la portée englobante"
def fonction_interne():
# L - Local
x_local = "Je suis locale"
# Python cherche 'x_local' : il le trouve dans la portée locale (L).
print(f"Interne voit local: {x_local}")
# Python cherche 'x_enclosing' : il ne le trouve pas en (L),
# il cherche donc en (E) et le trouve.
print(f"Interne voit enclosing: {x_enclosing}")
# Python cherche 'x_global' : il ne le trouve ni en (L) ni en (E),
# il cherche donc en (G) et le trouve.
print(f"Interne voit global: {x_global}")
fonction_interne()
fonction_externe()
3. Modifier des Variables de Portées Supérieures
Par défaut, vous pouvez lire des variables de portées supérieures, mais pas les modifier. Si vous assignez une valeur à une variable dans une fonction, Python suppose que c'est une nouvelle variable locale.
Le mot-clé global
Pour modifier une variable de la portée globale depuis une fonction, vous devez utiliser le mot-clé global.
compteur = 0 # Variable globale
def incrementer():
# Indique à Python que 'compteur' est la variable de la portée globale
global compteur
compteur += 1
print(f"Dans la fonction : {compteur}")
incrementer()
incrementer()
print(f"Hors de la fonction : {compteur}")
# Output:
# Dans la fonction : 1
# Dans la fonction : 2
# Hors de la fonction : 2
Attention : L'usage excessif de global est souvent considéré comme une mauvaise pratique car il rend le code plus difficile à suivre et à déboguer.
Le mot-clé nonlocal
Le mot-clé nonlocal est similaire à global, mais il est utilisé dans les fonctions imbriquées pour indiquer qu'une variable fait référence à une variable de la portée englobante (Enclosing), et non globale.
def fonction_externe():
compteur_englobant = 0
def fonction_interne():
# Indique que 'compteur_englobant' est celui de la portée (E)
nonlocal compteur_englobant
compteur_englobant += 1
return compteur_englobant
return fonction_interne # Retourne la fonction interne (closure)
# 'compteur' est maintenant une closure qui "se souvient" de son compteur_englobant
compteur = fonction_externe()
print(compteur()) # 1
print(compteur()) # 2
print(compteur()) # 3
nonlocal est essentiel pour créer des fonctions avec état, comme des compteurs ou des accumulateurs, sans utiliser de classes.
4. Portée des Blocs if, for, while
Contrairement à de nombreux autres langages (comme C++ ou Java), les blocs if, for, et while en Python ne créent pas de nouvelle portée locale.
for i in range(5):
x = i * 10
# 'i' et 'x' sont toujours accessibles ici, après la fin de la boucle.
print(f"Après la boucle, i = {i}") # i = 4
print(f"Après la boucle, x = {x}") # x = 40
if True:
y = "visible"
print(y) # "visible"
Seules les fonctions (def, lambda), les classes (class) et les modules créent de nouvelles portées en Python.
Les compréhensions de liste (depuis Python 3) ont leur propre portée pour la variable d'itération, ce qui évite les fuites.
z = 100
ma_liste = [z for z in range(5)]
print(ma_liste) # [0, 1, 2, 3, 4]
# La variable 'z' de la compréhension n'a pas écrasé la variable 'z' externe.
print(z) # 100
Conclusion
Comprendre la portée des variables est fondamental pour écrire du code Python correct et éviter des bugs subtils. La règle LEGB est le modèle mental à garder en tête pour savoir où Python va chercher une variable. Les mots-clés global et nonlocal sont les outils qui vous permettent de briser la règle de lecture seule et de modifier des variables dans des portées supérieures, mais ils doivent être utilisés avec discernement. Enfin, rappelez-vous que seuls def, class et les modules créent de vraies nouvelles portées.
Exercices :
Exercice 20 - Créateur de Compteur avec nonlocal
Objectif
Cet exercice a pour but de vous faire utiliser le mot-clé nonlocal pour créer une "closure" qui agit comme un compteur. Cela illustre comment une fonction interne peut modifier l'état de sa fonction parente.
Contexte
Une "closure" (fermeture) est une fonction qui se souvient de l'environnement dans lequel elle a été créée. En d'autres termes, elle a accès aux variables de la fonction qui l'a définie, même après que cette fonction parente a terminé son exécution.
En utilisant nonlocal, on peut créer des fonctions qui maintiennent un état interne, ce qui est une alternative légère à l'utilisation d'une classe pour des cas simples.
Énoncé
-
Créez un nouveau fichier Python nommé
counter_factory.py. -
Définissez une fonction "factory"
creer_compteur.- Cette fonction ne prend pas d'arguments.
- À l'intérieur de
creer_compteur, initialisez une variablecompteà0. C'est la variable de la portée englobante (Enclosing).
-
Définissez une fonction interne
incrementer.- Cette fonction doit être définie à l'intérieur de
creer_compteur. - Elle ne prend pas d'arguments.
- Utilisez le mot-clé
nonlocalpour déclarer que vous voulez modifier la variablecomptede la fonctioncreer_compteur. - Incrémentez
comptede 1. - La fonction
incrementerdoit retourner la nouvelle valeur decompte.
- Cette fonction doit être définie à l'intérieur de
-
La fonction
creer_compteurdoit retourner la fonctionincrementer. -
Testez votre créateur de compteurs.
- Dans le bloc principal (
if __name__ == '__main__':), créez un premier compteur en appelantcreer_compteur(). - Appelez ce premier compteur plusieurs fois et affichez le résultat pour vérifier qu'il s'incrémente correctement.
- Créez un deuxième compteur en appelant à nouveau
creer_compteur(). - Appelez ce deuxième compteur et vérifiez que son état est indépendant du premier.
- Dans le bloc principal (
Résultat Attendu
L'exécution de votre script de test doit montrer que chaque compteur maintient son propre état, indépendant des autres.
--- Compteur 1 ---
1
2
3
--- Compteur 2 ---
1
--- Compteur 1 (suite) ---
4
Cliquez ici pour voir un exemple de code de solution
# counter_factory.py
def creer_compteur():
"""
Une fonction factory qui crée et retourne une fonction compteur.
Chaque compteur a son propre état indépendant.
"""
# E - Portée Englobante (Enclosing Scope)
compte = 0
def incrementer():
"""
Fonction interne qui incrémente et retourne la variable 'compte'
de sa portée englobante.
"""
# L - Portée Locale (Local Scope)
# On déclare que 'compte' n'est pas une nouvelle variable locale,
# mais bien celle de la portée englobante.
nonlocal compte
compte += 1
return compte
# La factory retourne la fonction interne.
return incrementer
# --- Tests ---
if __name__ == "__main__":
# Crée une première instance de compteur.
compteur1 = creer_compteur()
print("--- Compteur 1 ---")
print(compteur1())
print(compteur1())
print(compteur1())
# Crée une deuxième instance. Elle aura son propre 'compte'.
compteur2 = creer_compteur()
print("\n--- Compteur 2 ---")
print(compteur2())
print("\n--- Compteur 1 (suite) ---")
print(compteur1())