# Le namespace local d'une fonction peut être inspecté
def f():
a = 1
b = 2
print(locals()) # dict des variables locales
f(){'a': 1, 'b': 2}
Quand vous écrivez x = 10, où cette variable x existe-t-elle ? Dans la fonction ? Dans le module ? Ailleurs ? Python suit une règle précise, la règle LEGB, pour résoudre les noms. Maîtriser cette règle évite des bugs mystérieux et permet d’écrire des closures et décorateurs solides. C’est un sujet fondamental au niveau Expert.
Un namespace (espace de noms) est simplement un dictionnaire qui associe des noms (variables, fonctions, classes) à des objets.
Quand vous écrivez x = 10, Python ajoute une entrée dans le namespace courant : {"x": 10}.
# Le namespace local d'une fonction peut être inspecté
def f():
a = 1
b = 2
print(locals()) # dict des variables locales
f(){'a': 1, 'b': 2}
# Le namespace global aussi
pays = "France"
langue = "Python"
# globals() renvoie le dict du namespace global (filtré pour affichage)
noms_interessants = {k: v for k, v in globals().items() if not k.startswith("_")}
# (On affiche juste les clés pertinentes)Python maintient plusieurs namespaces simultanément, dans une hiérarchie.
Quand Python rencontre un nom (comme x), il le cherche dans cet ordre :
| Niveau | Nom | Où |
|---|---|---|
| L | Local | Variables locales de la fonction courante |
| E | Enclosing | Variables des fonctions englobantes |
| G | Global | Variables du module |
| B | Built-in | Fonctions/noms prédéfinis de Python (print, len, int…) |
Dès qu’il trouve, il s’arrête. Si rien ne correspond, c’est NameError.
x = "global" # G : global
def externe():
x = "enclosing" # E : englobante
def interne():
x = "local" # L : local
print(f"Dans interne : {x}") # L trouvé d'abord
interne()
print(f"Dans externe : {x}") # E
externe()
print(f"Global : {x}") # GDans interne : local
Dans externe : enclosing
Global : global
Chaque fonction a son propre scope local qui cache les noms identiques des niveaux supérieurs.
Si un nom n’existe pas en local, Python remonte la chaîne.
x = "global"
def fonction():
# Pas de x local → remonte au global
print(x)
fonction()global
Les fonctions comme print, len, max, int… sont dans le niveau B (built-in).
# print est trouvé au niveau B
print("Hello")
# On peut le redéfinir (mauvaise idée !)
print = "pas une fonction"
# Restaurons
del print
print("Normal")Hello
Normal
Nommer une variable list, dict, str, max, sum, id, type… écrase la version built-in dans votre scope :
list = [1, 2, 3] # ❌ list n'est plus la classe
# Maintenant list([1, 2]) planterait !
# Nettoyer
del list
print(list([1, 2, 3]))[1, 2, 3]
Convention : si vous voulez vraiment un nom comme ça, ajoutez un underscore : list_, type_, sum_.
Dans une fonction, Python distingue fortement lecture et écriture.
x = 10 # global
def f():
print(x) # lit x depuis le global : OK
f()10
x = 10 # global
def f():
x = 20 # crée une variable LOCALE x (n'affecte pas le global)
print(f"Dans f : x = {x}")
f()
print(f"Global : x = {x}") # inchangé !Dans f : x = 20
Global : x = 10
C’est le point le plus subtil. Dès que Python voit x = ... dans une fonction, il considère x comme local à cette fonction. Pas de question.
x = 10
def f():
print(x) # tentative de lecture AVANT affectation
x = 20
f()--------------------------------------------------------------------------- UnboundLocalError Traceback (most recent call last) Cell In[9], line 7 3 def f(): 4 print(x) # tentative de lecture AVANT affectation 5 x = 20 6 ----> 7 f() Cell In[9], line 4, in f() 3 def f(): ----> 4 print(x) # tentative de lecture AVANT affectation 5 x = 20 UnboundLocalError: cannot access local variable 'x' where it is not associated with a value
UnboundLocalError : puisque x = 20 apparaît dans f, Python a classé x comme local pour toute la fonction. Le print(x) initial cherche donc x local… qui n’a pas encore été créé.
global et nonlocalglobal : accéder au niveau modulePour modifier une variable globale depuis une fonction :
compteur = 0
def incrementer():
global compteur # dit à Python : pas de local, utilise le global
compteur += 1
incrementer()
incrementer()
incrementer()
print(f"Compteur : {compteur}")Compteur : 3
Sans global, Python aurait créé une nouvelle variable locale compteur.
nonlocal : accéder au niveau enclosingPour modifier une variable d’une fonction englobante (pas globale) :
def compteur():
n = 0
def increment():
nonlocal n # accès à n de compteur (pas global !)
n += 1
return n
return increment
c = compteur()
print(c()) # 1
print(c()) # 2
print(c()) # 31
2
3
nonlocal ne peut viser qu’une variable de fonction englobante, pas le global.
global / nonlocal ?
Rarement ! En pratique :
global : code « pas très propre ». Préférez renvoyer une valeur ou passer un objet mutable.nonlocal : utile dans les closures et les décorateurs (vu chapitre 3).Le besoin d’global est souvent le signe qu’une classe serait plus appropriée.
Depuis Python 3, les compréhensions (liste, dict, set, générateur) créent leur propre scope local. Les variables d’itération ne fuient pas à l’extérieur.
x = 100
resultat = [x for x in range(5)] # x ici est local à la compréhension
print(resultat)
print(f"x globalement : {x}") # inchangé ![0, 1, 2, 3, 4]
x globalement : 100
En Python 2 (ancien), x aurait été modifié. En Python 3, c’est isolé — c’est un comportement plus sûr mais piégeux si on ne le sait pas.
for classique (par contraste)Attention : une boucle for « normale » fuit ses variables !
for i in range(5):
pass
# i existe encore après la boucle
print(f"i = {i}")i = 4
C’est une source classique de bugs : si vous comptiez sur i pour autre chose, la boucle l’a écrasé.
Déjà vu au chapitre 3 : une fonction interne capture le scope englobant.
def multiplicateur(facteur):
def multiplier(x):
return x * facteur # facteur est dans le scope E
return multiplier
doubler = multiplicateur(2)
tripler = multiplicateur(3)
# Les deux fonctions ont capturé leur propre facteur
print(doubler(10), tripler(10))20 30
Fait intéressant : chaque closure garde sa copie des variables capturées. doubler et tripler ne se partagent pas leur facteur.
L’attribut __closure__ d’une fonction révèle les variables capturées.
def externe():
a = 10
b = 20
def interne():
return a + b
return interne
f = externe()
# Noms capturés
print(f.__code__.co_freevars)
# Valeurs capturées
for cell in f.__closure__:
print(f" → {cell.cell_contents}")('a', 'b')
→ 10
→ 20
On l’a survolé au chapitre 3 (Partie 3). Approfondissons pourquoi ça se comporte ainsi.
# Créer 3 fonctions qui multiplient par 1, 2, 3
fonctions = []
for i in range(1, 4):
fonctions.append(lambda x: x * i)
# Surprise : toutes multiplient par 3 !
for f in fonctions:
print(f(10))30
30
30
Pourquoi ? Les 3 lambdas capturent la même variable i (pas sa valeur, mais la référence). Quand on appelle les fonctions, la boucle est terminée et i vaut 3.
Solution 1 : figer la valeur avec un paramètre par défaut.
fonctions = [lambda x, i=i: x * i for i in range(1, 4)]
for f in fonctions:
print(f(10))10
20
30
i=i évalue i au moment de la définition de la lambda, et stocke cette valeur comme défaut.
Solution 2 : une fonction créatrice explicite.
def fabrique(n):
return lambda x: x * n # closure sur n local, différent à chaque appel
fonctions = [fabrique(i) for i in range(1, 4)]
for f in fonctions:
print(f(10))10
20
30
Chaque appel à fabrique(i) crée un nouveau scope local avec sa propre variable n.
Les classes ont un scope propre, mais il se comporte différemment des fonctions.
x = "global"
class MaClasse:
x = "de classe"
def methode(self):
print(x) # ⚠️ cherche x dans le global, PAS dans la classe
obj = MaClasse()
obj.methode()global
Le scope d’une classe n’est pas accessible directement depuis ses méthodes. Pour accéder aux attributs de classe, il faut self.x ou MaClasse.x.
class MaClasse:
x = "de classe"
def methode(self):
print(self.x) # OK
print(MaClasse.x) # OK aussi
obj = MaClasse()
obj.methode()de classe
de classe
Les classes sont des namespaces, pas des scopes imbriqués au sens LEGB. C’est un détail d’implémentation hérité des premières versions de Python. Retenez simplement : utilisez self. ou le nom de la classe pour accéder aux attributs depuis les méthodes.
Python expose locals() et globals() qui renvoient les namespaces actuels sous forme de dict.
locals()def f():
a = 1
b = 2
noms = list(locals().keys())
print(f"Variables locales : {noms}")
f()Variables locales : ['a', 'b']
globals()a_global = 42
b_global = "hello"
# On filtre pour ne garder que nos variables (pas les builtins Python)
noms_globaux = [k for k in globals() if not k.startswith("_") and k not in ("In", "Out", "get_ipython", "exit", "quit")]
print("Noms globaux (filtrés) :", [n for n in noms_globaux if n.startswith("a_") or n.startswith("b_")])Noms globaux (filtrés) : ['a_global', 'b_global']
locals() et globals() sont utiles pour l’introspection et le debug, mais ne devraient pas être utilisés couramment dans du code normal. Leur usage dénote souvent un design à revoir.
┌─────────────────────────────────────────────┐
│ B — Built-in (print, len, int, ...) │
│ ┌───────────────────────────────────────┐ │
│ │ G — Global (module entier) │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ E — Enclosing (fonction père) │ │ │
│ │ │ ┌───────────────────────────┐ │ │ │
│ │ │ │ L — Local (fonction fils) │ │ │ │
│ │ │ └───────────────────────────┘ │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
Recherche de noms : de l’intérieur vers l’extérieur (L → E → G → B). Premier trouvé = utilisé.
Dans quel ordre Python cherche-t-il un nom ?
c) Local → Enclosing → Global → Built-in — la règle LEGB. Premier trouvé, premier servi.
Que s’affiche-t-il ?
x = 10
def f():
x = 20
print(x)
f()
print(x)20 2010 2020 10c) 20 10 — x = 20 dans f crée une variable locale. Le x global reste à 10.
x = 10
def f():
x = 20
print(x)
f()
print(x)20
10
Que s’affiche-t-il ?
x = 10
def f():
print(x)
x = 20
f()1020UnboundLocalErrorNameErrorc) UnboundLocalError — Python voit x = 20 plus bas et décide que x est local pour toute la fonction. Le print(x) essaie alors de lire x local avant qu’il soit affecté.
x = 10
def f():
print(x)
x = 20
f()--------------------------------------------------------------------------- UnboundLocalError Traceback (most recent call last) Cell In[24], line 7 3 def f(): 4 print(x) 5 x = 20 6 ----> 7 f() Cell In[24], line 4, in f() 3 def f(): ----> 4 print(x) 5 x = 20 UnboundLocalError: cannot access local variable 'x' where it is not associated with a value
Quel mot-clé permet de modifier une variable globale depuis une fonction ?
nonlocalglobalexternb) global — sans lui, une affectation crée une variable locale.
x = 0
def f():
global x
x = 42
f()
print(x)42
Que s’affiche-t-il ?
def f():
i = 100
resultat = [i for i in range(3)]
print(i, resultat)
f()2 [0, 1, 2]100 [0, 1, 2]Erreur0 [0, 1, 2]b) 100 [0, 1, 2] — les compréhensions ont leur propre scope en Python 3. Le i de la compréhension n’écrase pas le i de la fonction.
def f():
i = 100
resultat = [i for i in range(3)]
print(i, resultat)
f()100 [0, 1, 2]
Que s’affiche-t-il ?
def externe():
x = 1
def interne():
nonlocal x
x += 10
interne()
return x
print(externe())11011c) 11 — nonlocal x permet à interne de modifier le x de externe.
def externe():
x = 1
def interne():
nonlocal x
x += 10
interne()
return x
print(externe())11
Que s’affiche-t-il ?
fs = [lambda: i for i in range(3)]
print([f() for f in fs])[0, 1, 2][2, 2, 2][3, 3, 3][0, 0, 0]b) [2, 2, 2] — piège de la capture : toutes les lambdas partagent le même i, qui vaut 2 à la fin de la compréhension.
fs = [lambda: i for i in range(3)]
print([f() for f in fs])
# Correction : figer la valeur
fs_ok = [lambda i=i: i for i in range(3)]
print([f() for f in fs_ok])[2, 2, 2]
[0, 1, 2]
Depuis une méthode, comment accéder à un attribut de classe ?
self.attribut ou NomClasse.attributglobal attributb) — le scope d’une classe n’est pas accessible directement depuis ses méthodes. Il faut self. ou le nom de la classe.
Écrivez une fonction compteur(debut=0, pas=1) qui renvoie une fonction interne qui incrémente et renvoie un compteur à chaque appel.
def compteur(debut=0, pas=1):
...
c = compteur()
print(c(), c(), c()) # 1 2 3
c2 = compteur(10, 5)
print(c2(), c2(), c2()) # 15 20 25def compteur(debut=0, pas=1):
n = debut
def incrementer():
nonlocal n
n += pas
return n
return incrementer
c1 = compteur()
print(c1(), c1(), c1())
c2 = compteur(10, 5)
print(c2(), c2(), c2())
# Les deux compteurs sont indépendants
print(c1()) # continue de c11 2 3
15 20 25
4
Sans exécuter, prédisez ce qui s’affiche pour chaque extrait. Puis vérifiez.
# Extrait A
x = 1
def f():
x = 2
def g():
x = 3
g()
return x
print(f())
# Extrait B
x = 1
def f():
x = 2
def g():
nonlocal x
x = 3
g()
return x
print(f())
# Extrait C
x = 1
def f():
def g():
global x
x = 3
g()
return x
print(f(), x)
# Extrait D
liste = [1, 2, 3]
def f():
liste.append(4) # sans global, ça marche ?
f()
print(liste)A → 2. Chaque fonction a son x local, indépendants.
x = 1
def f():
x = 2
def g():
x = 3
g()
return x
print(f())2
B → 3. nonlocal x permet à g de modifier le x de f.
x = 1
def f():
x = 2
def g():
nonlocal x
x = 3
g()
return x
print(f())3
C → 1 3. global x fait que g modifie le x global. Mais f n’a pas de x local (il n’y est référencé nulle part), donc return x remonte… au global, qui vaut maintenant 3 — attendez, mais au moment du return x, il vient d’être mis à 3. Regardons :
x = 1
def f():
def g():
global x
x = 3
g()
return x # lit x global = 3
print(f(), x)3 3
Les deux valeurs sont 3 — car g() a modifié le global.
D → [1, 2, 3, 4]. Pas besoin de global pour modifier un objet mutable ; on ne réaffecte pas liste, on appelle une méthode dessus.
liste = [1, 2, 3]
def f():
liste.append(4)
f()
print(liste)[1, 2, 3, 4]
liste = ... dans une fonction : réaffectation → crée une variable locale sauf si global.liste.append(...) : mutation d’objet → accède à l’objet global (ou englobant) sans souci.Cette distinction explique pourquoi on peut modifier un dict/liste global depuis une fonction sans global.
Ce code devrait générer des fonctions qui ajoutent respectivement 1, 2 et 3 à leur argument. Il est buggué. Corrigez-le.
# Version buggée
additionneurs = [lambda x: x + n for n in range(1, 4)]
for f in additionneurs:
print(f(10)) # attendu : 11, 12, 13
# Correction ?Version buggée (comportement actuel) :
additionneurs = [lambda x: x + n for n in range(1, 4)]
for f in additionneurs:
print(f(10)) # tous renvoient 13 (n=3 à la fin)13
13
13
Correction 1 — paramètre par défaut :
additionneurs = [lambda x, n=n: x + n for n in range(1, 4)]
for f in additionneurs:
print(f(10))11
12
13
Correction 2 — fonction factrice :
def fabrique_additionneur(n):
return lambda x: x + n
additionneurs = [fabrique_additionneur(n) for n in range(1, 4)]
for f in additionneurs:
print(f(10))11
12
13
Les deux versions marchent. La première est plus concise, la seconde plus lisible si la logique est complexe.
Créez creer_validateur(type_attendu) qui renvoie une fonction qui vérifie si son argument est du type attendu. Utilisez une closure.
def creer_validateur(type_attendu):
...
est_entier = creer_validateur(int)
est_chaine = creer_validateur(str)
print(est_entier(42)) # True
print(est_entier("42")) # False
print(est_chaine("hello")) # Truedef creer_validateur(type_attendu):
def valider(valeur):
return isinstance(valeur, type_attendu)
return valider
est_entier = creer_validateur(int)
est_chaine = creer_validateur(str)
est_liste = creer_validateur(list)
# Tests
for v in [42, "42", [1, 2], 3.14]:
print(f"{v!r:<10} entier={est_entier(v):<6} chaine={est_chaine(v):<6} liste={est_liste(v)}")42 entier=1 chaine=0 liste=False
'42' entier=0 chaine=1 liste=False
[1, 2] entier=0 chaine=0 liste=True
3.14 entier=0 chaine=0 liste=False
def creer_validateur_plusieurs(*types_acceptes):
"""Accepte plusieurs types."""
def valider(valeur):
return isinstance(valeur, types_acceptes)
return valider
est_nombre = creer_validateur_plusieurs(int, float)
print(est_nombre(42))
print(est_nombre(3.14))
print(est_nombre("42"))True
True
False
Les closures permettent de paramétrer des fonctions sans passer par des classes — c’est une forme légère de POO parfois très élégante.
global/nonlocal.global x : modifier une variable du module. Rare en bon code.nonlocal x : modifier une variable du scope englobant (closures). Plus courant.UnboundLocalError : une affectation plus loin rend la variable locale pour toute la fonction.for classiques : les variables fuient (à l’inverse des compréhensions).lambda x=x: ... pour figer la valeur.self. ou le nom de classe.list, max, id, …).← Chapitre précédent : Décorateurs • Chapitre suivant : Modules sys, os, pathlib →