backend / python
Lambda, closures et portée des variables
Explication
Ce que vous allez apprendre
- Écrire une lambda pour un usage court, et savoir quand préférer une fonction
defclassique - Suivre la règle LEGB pour comprendre où Python va chercher un nom de variable
- Construire une closure et comprendre pourquoi
nonlocalest 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ée | Exemple |
|---|---|---|
| L | Local | Variable définie dans la fonction courante |
| E | Enclosing (englobante) | Variable de la fonction qui contient celle-ci |
| G | Global | Variable définie au niveau du module |
| B | Built-in | Nom 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.
# 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 15Résumé
- La résolution de noms suit la règle LEGB : Local, Enclosing, Global, Built-in.
nonlocalpermet de modifier une variable d'une portée englobante,globalune variable de module.- Une closure capture les variables, pas leurs valeurs : attention aux boucles avec
lambda.
Exercices pratiques
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.