Les fonctions imbriquées, les closures et nonlocal

Objectifs de la leçon
  • 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 sans global.

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).

Analogie : une lettre dans une enveloppe

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éclarationLa variable appartient à…
global xLe niveau du fichier
nonlocal xLa 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     9

Analyse du code ligne par ligne

LigneExplication
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, Avi

make_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
1

Chaque 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 value

Pourquoi ? À 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     9
Afficher 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     9

line 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, Avi
Afficher 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, Avi

make_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: changed

change 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
6
Afficher 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
6
  • step n’est que lu dans increment, il n’a donc besoin d’aucune déclaration.
  • count change, il a donc besoin de nonlocal count.

Exercice 5: Sans nonlocal

def make_counter():
    count = 0
    def increment():
        count += 1
        return count
    return increment

counter = make_counter()
counter()
  1. Quelle erreur obtiendrez-vous, et pourquoi ?
  2. Quelle est la différence entre global et nonlocal ?
Afficher la solution
  1. UnboundLocalError. À cause de l’affectation count += 1, Python décide que count est une variable locale de increment — et elle n’a pas de valeur quand on essaie de l’augmenter. La correction : nonlocal count au début de increment.
  2. global — la variable appartient au niveau du fichier. nonlocal — la variable appartient à la fonction englobante la plus proche. Une closure avec nonlocal donne à 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, sinon UnboundLocalError.
  • 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 return en sortie, c’est mieux.

Documentation officielle