Les fonctions imbriquées, les closures et nonlocal
- Définir une fonction dans une fonction et expliquer ce qu’elle voit de la portée Enclosing
- Expliquer ce qu’est une closure — une fonction qui se souvient de variables même après la fin de la fonction extérieure
- Comprendre pourquoi modifier une variable extérieure provoque une UnboundLocalError, et corriger avec nonlocal
- Distinguer global et nonlocal
Qu’allons-nous apprendre ?
- La fonction imbriquée (Nested Function) — une fonction définie dans une fonction.
- Enclosing — le E de LEGB, de près.
- La closure — une fonction qui « se souvient » de variables.
nonlocal— comment modifier une variable de la fonction englobante.
Pourquoi en avons-nous besoin ?
- Une fonction d’aide privée : parfois une fonction a besoin d’une petite fonction d’aide qui n’appartient qu’à elle. Il n’y a aucune raison qu’elle soit globale et se mélange au reste du code.
- Des fonctions qui se souviennent d’un état : dans la leçon précédente, nous avons vu que
make_multiplier(2)renvoie une fonction qui « se souvient » du nombre 2. Aujourd’hui, nous comprendrons comment cela fonctionne — et comment construire un compteur, un accumulateur ou un cache sansglobal.
Explication à partir des bases
Une fonction dans une fonction
On peut définir un def à l’intérieur d’un def. La fonction intérieure :
- N’existe qu’à l’intérieur de la fonction extérieure — on ne peut pas l’appeler de l’extérieur.
- Voit les variables et les paramètres de la fonction extérieure.
Enclosing — le E de LEGB
Dans la leçon sur la portée, nous avons appris l’ordre de recherche L → E → G → B. Quand une fonction intérieure utilise un nom, Python le cherche :
L Local → inside the inner function
E Enclosing → inside the outer function
G Global → top level of the file
B Built-in → print, len, ...La closure — une fonction qui se souvient
Quand une fonction extérieure renvoie la fonction intérieure, il se passe quelque chose d’intéressant : la fonction extérieure est déjà terminée, mais la fonction intérieure se souvient encore de ses variables. Cette combinaison — une fonction + les variables dont elle se souvient — s’appelle une closure (fermeture).
La fonction extérieure écrit une lettre (la fonction intérieure) et la met dans une enveloppe avec un petit mot contenant quelques données (les variables). Elle envoie l’enveloppe et disparaît — mais celui qui ouvre l’enveloppe reçoit à la fois la lettre et le petit mot. Chaque enveloppe a son propre petit mot.
nonlocal
Lire une variable de la portée Enclosing — c’est toujours permis. La modifier (lui affecter une nouvelle valeur) — ce n’est pas possible sans déclaration, exactement comme avec une globale. La déclaration est nonlocal name : « cette variable appartient à la fonction englobante ».
| Déclaration | La variable appartient à… |
|---|---|
global x | Le niveau du fichier |
nonlocal x | La fonction englobante la plus proche |
Syntaxe
def outer():
count = 0 # enclosing variable
def inner():
nonlocal count # assignments go to outer's count
count += 1
return count
return inner # return the function itself (no parentheses)Premier exemple de code
Une fonction d’aide privée. week-2/receipt.py :
def format_receipt(items):
def line(name, price):
return name.ljust(10) + str(price)
lines = []
for name, price in items:
lines.append(line(name, price))
return "\n".join(lines)
print(format_receipt([("Milk", 6), ("Bread", 9)]))Sortie :
Milk 6
Bread 9Analyse du code ligne par ligne
| Ligne | Explication |
|---|---|
def line(name, price): | Une fonction intérieure — n’existe que dans format_receipt. |
name.ljust(10) | Complète le nom avec des espaces jusqu’à 10 caractères, pour que les prix soient alignés en colonne. |
lines.append(line(name, price)) | La fonction extérieure utilise sa fonction d’aide. |
"\n".join(lines) | Joint toutes les lignes en un seul texte, avec un saut de ligne entre elles. |
Un autre exemple
Une closure — une fonction qui se souvient. week-2/greeter.py :
def make_greeter(greeting):
def greet(name):
return greeting + ", " + name
return greet
hello = make_greeter("Hello")
shalom = make_greeter("Shalom")
print(hello("Dana"))
print(shalom("Avi"))Sortie :
Hello, Dana
Shalom, Avimake_greeter s’est déjà terminée à la ligne hello = ..., mais hello se souvient encore que greeting vaut "Hello". Chaque fonction créée a sa propre copie de la variable — shalom se souvient de "Shalom".
Un compteur avec nonlocal. week-2/counter.py :
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter = make_counter()
print(counter(), counter(), counter())
other = make_counter()
print(other())Sortie :
1 2 3
1Chaque compteur se souvient de son propre count : other part de zéro, indépendamment de counter. C’est un état privé — sans variable globale que tout le code peut modifier.
Erreur courante
L’erreur : modifier une variable de la fonction englobante sans nonlocal.
def make_counter():
count = 0
def increment():
count += 1
return count
return increment
counter = make_counter()
counter()UnboundLocalError: cannot access local variable 'count' where it is not associated with a valuePourquoi ? À cause de l’affectation count += 1, Python décide que count est une variable locale de increment — puis essaie de lire une variable locale qui n’a pas encore de valeur. C’est exactement la même erreur que celle que nous avons vue avec les globales dans la leçon sur la portée.
La correction : nonlocal count au début de increment.
Exercices
Les exercices n’utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d’abord seul, et n’ouvrez la solution qu’ensuite.
Exercice 1: Une fonction d’aide intérieure
Écrivez une fonction format_receipt(items) qui reçoit une liste de paires (nom, prix) et renvoie le texte d’un ticket. À l’intérieur, définissez une fonction intérieure line(name, price) qui complète le nom jusqu’à 10 caractères (ljust).
print(format_receipt([("Milk", 6), ("Bread", 9)]))Résultat attendu :
Milk 6
Bread 9Afficher la solution
def format_receipt(items):
def line(name, price):
return name.ljust(10) + str(price)
lines = []
for name, price in items:
lines.append(line(name, price))
return "\n".join(lines)
print(format_receipt([("Milk", 6), ("Bread", 9)]))Milk 6
Bread 9line n’existe que dans format_receipt — c’est une fonction d’aide privée, qui ne se mélange pas au reste du code.
Exercice 2: Closure
Écrivez make_greeter(greeting) qui renvoie une fonction qui reçoit un nom et renvoie une salutation.
hello = make_greeter("Hello")
shalom = make_greeter("Shalom")
print(hello("Dana"))
print(shalom("Avi"))Résultat attendu :
Hello, Dana
Shalom, AviAfficher la solution
def make_greeter(greeting):
def greet(name):
return greeting + ", " + name
return greet
hello = make_greeter("Hello")
shalom = make_greeter("Shalom")
print(hello("Dana"))
print(shalom("Avi"))Hello, Dana
Shalom, Avimake_greeter est déjà terminée, mais chaque fonction qu’elle a renvoyée se souvient de son propre greeting. C’est une closure.
Exercice 3: Prédire la sortie
def outer():
message = "outer"
def show():
print("show:", message)
def change():
nonlocal message
message = "changed"
show()
change()
show()
outer()Afficher la solution
def outer():
message = "outer"
def show():
print("show:", message)
def change():
nonlocal message
message = "changed"
show()
change()
show()
outer()show: outer
show: changedchange utilise nonlocal, donc l’affectation modifie le message de outer — elle ne crée pas de nouvelle variable locale.
Exercice 4: Un compteur avec un pas
Écrivez make_counter(step) — chaque appel au compteur l’augmente de step et renvoie la nouvelle valeur. Créez deux compteurs et vérifiez qu’ils ne s’influencent pas l’un l’autre.
by_two = make_counter(2)
by_ten = make_counter(10)
print(by_two(), by_two())
print(by_ten())
print(by_two())Résultat attendu :
2 4
10
6Afficher la solution
def make_counter(step):
count = 0
def increment():
nonlocal count
count += step
return count
return increment
by_two = make_counter(2)
by_ten = make_counter(10)
print(by_two(), by_two())
print(by_ten())
print(by_two())2 4
10
6stepn’est que lu dansincrement, il n’a donc besoin d’aucune déclaration.countchange, il a donc besoin denonlocal count.
Exercice 5: Sans nonlocal
def make_counter():
count = 0
def increment():
count += 1
return count
return increment
counter = make_counter()
counter()- Quelle erreur obtiendrez-vous, et pourquoi ?
- Quelle est la différence entre
globaletnonlocal?
Afficher la solution
UnboundLocalError. À cause de l’affectationcount += 1, Python décide quecountest une variable locale deincrement— et elle n’a pas de valeur quand on essaie de l’augmenter. La correction :nonlocal countau début deincrement.global— la variable appartient au niveau du fichier.nonlocal— la variable appartient à la fonction englobante la plus proche. Une closure avecnonlocaldonne à chaque compteur un état privé, sans variable globale que tout le code peut modifier.
Résumé
- Fonction imbriquée — une fonction dans une fonction. Elle n’existe qu’à l’intérieur, et voit les variables de la fonction englobante (Enclosing).
- Closure — une fonction intérieure renvoyée, qui se souvient des variables de la fonction qui l’a créée. Chacune a sa propre copie.
- Lire une variable de la portée Enclosing — c’est permis. La modifier — seulement avec
nonlocal, sinonUnboundLocalError. global— une variable au niveau du fichier.nonlocal— une variable dans la fonction englobante.- Une closure donne un état privé sans variable globale. Et pourtant — quand c’est possible, un paramètre en entrée et un
returnen sortie, c’est mieux.