Les fermetures en Python
Découvrez comment fonctionnent les fermetures Python : fonctions imbriquées, variables capturées, nonlocal, pièges courants et cas d'utilisation pratiques.
Une fermeture est une fonction imbriquée qui mémorise les variables de la portée englobante dans laquelle elle a été définie, même après que la fonction externe a retourné. Les fermetures permettent d'attacher un état privé à une fonction sans utiliser de classe — ce qui en fait l'un des outils les plus élégants de Python pour construire des callbacks, des fabriques et des aides avec état.
Cette page explique le fonctionnement des fermetures, les trois conditions qu'elles requièrent, les pièges courants et les cas d'utilisation pratiques.
Qu'est-ce qu'une fermeture ?
Lorsque Python exécute une fonction, il crée une portée locale qui disparaît une fois la fonction retournée. Normalement, toute variable définie à cet endroit est perdue. Une fermeture fait exception : si une fonction interne référence une variable d'une fonction externe, Python maintient cette variable en vie dans un objet cellule spécial, et la fonction interne transporte une référence à ces cellules partout où elle va.
La fermeture la plus simple est une fabrique de fonctions — une fonction qui construit et retourne une autre fonction :
def make_multiplier(factor):
def multiply(n):
return n * factor # 'factor' is captured from the enclosing scope
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
print(double(10)) # 20Chaque appel à make_multiplier crée une nouvelle fermeture avec sa propre copie indépendante de factor. double et triple sont totalement indépendants même s'ils ont été créés par la même fonction.
Trois conditions pour une fermeture
Une fonction est une fermeture lorsque ces trois conditions sont réunies :
- Il existe une fonction imbriquée — une fonction définie à l'intérieur d'une autre fonction.
- La fonction imbriquée référence une variable de la portée englobante — cette variable est appelée une variable libre.
- La fonction englobante retourne la fonction imbriquée (ou la transmet ailleurs).
def outer():
message = 'Hello from outer' # free variable
def inner():
print(message) # inner references it
return inner # outer returns inner
greet = outer()
greet() # Hello from outerAprès le retour de outer(), son cadre local disparaît — mais message survit à l'intérieur de greet.__closure__.
Inspecter une fermeture
Python expose les cellules de fermeture via l'attribut __closure__ :
def make_adder(n):
def add(x):
return x + n
return add
add5 = make_adder(5)
print(add5(3)) # 8
print(add5.__closure__) # (<cell at 0x...>,)
print(add5.__closure__[0].cell_contents) # 5__closure__ est un tuple d'objets cellules — un par variable capturée. Si une fonction n'est pas une fermeture, __closure__ vaut None.
Modifier des variables capturées avec nonlocal
Par défaut, vous pouvez lire une variable capturée mais pas la relier. Tenter de lui assigner une valeur crée une nouvelle variable locale à la place, ce qui n'est généralement pas ce que vous souhaitez. Utilisez le mot-clé nonlocal pour indiquer à Python que vous visez la variable de la portée englobante :
def make_counter(start=0):
count = start
def increment(step=1):
nonlocal count # rebind the enclosing 'count', not a new local
count += step
return count
return increment
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
print(counter(5)) # 7
counter2 = make_counter(10)
print(counter2()) # 11
print(counter()) # 8 — counter is unaffectedChaque appel à make_counter produit une cellule count indépendante. counter et counter2 ne partagent pas d'état.
Pour un examen plus approfondi de la façon dont Python détermine à quelle portée appartient une variable, consultez Python Scope.
Piège courant : les fermetures dans les boucles
Une erreur classique consiste à créer des fermetures dans une boucle en supposant que chacune capture la valeur courante de la variable de boucle :
# Wrong — all functions capture the same 'i' cell
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs]) # [2, 2, 2] — not [0, 1, 2]Les trois lambdas partagent une seule cellule qui contient la variable de boucle i. Au moment où elles sont appelées, i a atteint sa valeur finale de 2.
Solution 1 : argument par défaut (capture par valeur)
funcs = []
for i in range(3):
funcs.append(lambda i=i: i) # default arg is evaluated immediately
print([f() for f in funcs]) # [0, 1, 2]Solution 2 : fonction fabrique
def make_func(i):
def f():
return i
return f
funcs = [make_func(i) for i in range(3)]
print([f() for f in funcs]) # [0, 1, 2]La fonction fabrique crée une nouvelle portée — et donc une nouvelle cellule — à chaque itération. C'est l'approche la plus explicite et la plus lisible.
Cas d'utilisation pratiques
Application partielle
Les fermetures sont une alternative légère à functools.partial lorsque vous avez besoin d'une version préconfigurée d'une fonction :
def make_power(exponent):
def power(base):
return base ** exponent
return power
square = make_power(2)
cube = make_power(3)
print(square(4)) # 16
print(cube(3)) # 27Mémoïsation simple
Une fermeture peut contenir un dictionnaire de cache qui persiste entre les appels :
def make_memoized(func):
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@make_memoized
def slow_square(n):
return n * n
print(slow_square(4)) # 16
print(slow_square(4)) # 16 (served from cache)
print(slow_square(7)) # 49Ce schéma est exactement celui des décorateurs Python sous le capot — un décorateur n'est qu'une fermeture qui enveloppe une autre fonction.
Configuration de callback
Les fermetures sont pratiques pour construire des callbacks qui nécessitent un peu de contexte intégré :
def make_logger(prefix):
def log(message):
print(f'[{prefix}] {message}')
return log
info = make_logger('INFO')
error = make_logger('ERROR')
info('Server started') # [INFO] Server started
error('Disk full') # [ERROR] Disk fullFermetures vs. classes
Une fermeture et une classe à méthode unique résolvent souvent le même problème. Choisissez en fonction de la complexité :
| Situation | Préférer |
|---|---|
| Un état, un comportement | Fermeture |
| Plusieurs méthodes ou attributs publics | Classe |
| Doit être sérialisé (ex. pickle) | Classe |
| Passer un callback à une autre fonction | Fermeture |
# Class approach
class Counter:
def __init__(self, start=0):
self.count = start
def increment(self, step=1):
self.count += step
return self.count
# Closure approach
def make_counter(start=0):
count = start
def increment(step=1):
nonlocal count
count += step
return count
return incrementLes deux produisent un comportement identique. La fermeture est plus concise ; la classe est plus facile à découvrir et à étendre.
Conclusion
Les fermetures permettent à une fonction imbriquée de porter son propre état privé en mémorisant les variables de la portée où elle a été définie. Les points clés sont :
- Une fermeture requiert une fonction imbriquée, une variable libre, et que la fonction interne soit retournée ou transmise.
- Utilisez
nonlocallorsque vous devez relier (pas seulement lire) une variable capturée. - Évitez le piège de la variable de boucle : utilisez une fonction fabrique ou un argument par défaut pour capturer la valeur à chaque itération.
- Les fermetures sont le fondement des décorateurs et sont étroitement liées aux règles de portée de Python.