Retour au cours

backend / python

Lambda, closures et portée des variables

Leçon 91 exercice

Explication

Ce que vous allez apprendre

  • Écrire une lambda pour un usage court, et savoir quand préférer une fonction def classique
  • Suivre la règle LEGB pour comprendre où Python va chercher un nom de variable
  • Construire une closure et comprendre pourquoi nonlocal est nécessaire pour la modifier
  • Identifier et corriger le piège classique des lambdas créées dans une boucle
  • Utiliser une closure pour fabriquer des compteurs indépendants ou des fonctions paramétrées

Dans quel contexte ?

Un développeur génère dynamiquement une liste de callbacks pour des boutons d'interface, un par catégorie de produit, avec callbacks.append(lambda: filtrer(categorie)) à l'intérieur d'une boucle for categorie in categories. Au clic, tous les boutons filtrent sur la même dernière catégorie au lieu de la leur : un classique de la capture tardive des variables par les closures, que cette leçon explique et corrige.

Une fonction sans nom, pour un usage jetable

Une lambda est une fonction anonyme limitée à une seule expression, pensée pour des usages courts et immédiats, typiquement comme argument d'une autre fonction (sorted(..., key=lambda x: ...)). Ce n'est pas un raccourci systématique : dès que la logique dépasse une ligne ou mérite d'être réutilisée ailleurs, une fonction classique avec def est préférable, ne serait-ce que parce qu'elle porte un nom qui documente son intention.

Où Python va-t-il chercher un nom de variable

Quand Python rencontre un nom de variable, il le cherche dans un ordre précis résumé par l'acronyme LEGB : d'abord la portée locale, puis les portées englobantes, puis la portée globale du module, et enfin les objets intégrés au langage. Comprendre cet ordre explique pourquoi une variable définie dans une fonction n'existe pas en dehors d'elle, et pourquoi une fonction imbriquée voit les variables de la fonction qui la contient.

Lettre (LEGB)PortéeExemple
LLocalVariable définie dans la fonction courante
EEnclosing (englobante)Variable de la fonction qui contient celle-ci
GGlobalVariable définie au niveau du module
BBuilt-inNom intégré au langage (len, print...)

Les closures : une fonction qui se souvient

Une closure est une fonction interne qui continue d'avoir accès aux variables de la fonction qui l'a créée, même après que celle-ci a terminé son exécution. C'est ce mécanisme qui permet de fabriquer des compteurs indépendants ou des fonctions paramétrées à la volée. nonlocal est le mot-clé nécessaire pour modifier, et pas seulement lire, une variable capturée.

Le piège classique des boucles

Créer des lambdas dans une boucle en capturant la variable de boucle est une erreur récurrente : toutes les lambdas partagent la même variable, et non sa valeur au moment de leur création. On corrige cela en forçant la capture immédiate via un argument par défaut (lambda i=i: i).

Piège fréquent

[lambda: i for i in range(3)] ne produit pas trois fonctions renvoyant 0, 1 et 2 : toutes partagent la même variable i, qui vaut 2 à la fin de la boucle. La correction : [lambda i=i: i for i in range(3)], qui capture la valeur au moment de la création grâce à l'argument par défaut.

Commandes & code

Lambda, closures et portée des variables

Fonctions anonymes et fonctions qui "capturent" leur environnement.

python
# Lambda : fonction anonyme, une seule expression
carre = lambda x: x ** 2
addition = lambda a, b: a + b

# Usage typique : callback pour sort/filter/map
personnes = [("Ana", 30), ("Bo", 25), ("Cy", 22)]
personnes.sort(key=lambda p: p[1])

pairs = list(filter(lambda x: x % 2 == 0, range(10)))
carres = list(map(lambda x: x ** 2, range(5)))

# Règle des 4 portées : LEGB (Local, Enclosing, Global, Built-in)
x = "global"

def externe():
    x = "enclosing"

    def interne():
        x = "local"
        print(x)          # "local" : trouve dans la portee locale d'abord

    interne()
    print(x)               # "enclosing"

externe()
print(x)                   # "global"

# Closures : une fonction interne "capture" les variables de la fonction externe
def creer_compteur():
    compte = 0
    def incrementer():
        nonlocal compte     # necessaire pour MODIFIER une variable enclosing
        compte += 1
        return compte
    return incrementer

compteur1 = creer_compteur()
compteur2 = creer_compteur()
print(compteur1(), compteur1(), compteur1())    # 1 2 3
print(compteur2())                               # 1 -- etat independant !

# global : modifier une variable au niveau module depuis une fonction
total = 0
def ajouter_au_total(valeur):
    global total
    total += valeur

# PIEGE classique : closures dans une boucle (late binding)
fonctions = []
for i in range(3):
    fonctions.append(lambda: i)          # capture la VARIABLE i, pas sa valeur

print([f() for f in fonctions])          # [2, 2, 2] -- pas [0, 1, 2] !

# Solution : forcer la capture par valeur via un argument par defaut
fonctions_correctes = []
for i in range(3):
    fonctions_correctes.append(lambda i=i: i)

print([f() for f in fonctions_correctes])   # [0, 1, 2]

# Closures pour la memoisation manuelle (avant de decouvrir functools.cache)
def memoiser(fonction):
    cache = {}
    def wrapper(n):
        if n not in cache:
            cache[n] = fonction(n)
        return cache[n]
    return wrapper

def fibonacci(n):
    return n if n < 2 else fibonacci(n - 1) + fibonacci(n - 2)

fibonacci_rapide = memoiser(fibonacci)

# Closures pour construire des fonctions parametrees (factory pattern)
def creer_multiplicateur(facteur):
    return lambda x: x * facteur

doubler = creer_multiplicateur(2)
tripler = creer_multiplicateur(3)
print(doubler(5), tripler(5))    # 10 15

Résumé

  • La résolution de noms suit la règle LEGB : Local, Enclosing, Global, Built-in.
  • nonlocal permet de modifier une variable d'une portée englobante, global une variable de module.
  • Une closure capture les variables, pas leurs valeurs : attention aux boucles avec lambda.

Exercices pratiques

1 disponible
1

Mission : réparer des boutons de filtre qui pointent tous sur la même catégorie

Objectif : Diagnostiquer le piège de la capture tardive des closures dans une boucle, et concevoir des compteurs indépendants avec nonlocal.

Contexte

Une interface génère dynamiquement des callbacks de filtre pour chaque catégorie de produit avec callbacks.append(lambda: filtrer(categorie)) à l'intérieur d'une boucle for categorie in categories. Au clic, tous les boutons filtrent sur la dernière catégorie de la liste, peu importe celui sur lequel l'utilisateur clique.

Tu dois expliquer précisément ce mécanisme de capture tardive, corriger le code, puis construire deux compteurs indépendants à l'aide de closures et nonlocal.

Résoudre l’exercice →