Comprendre le concept de Fermeture (Closure)
Une fermeture, ou closure, survient lorsqu'une fonction interne accède aux variables définies dans la portée d'une fonction parente, même après que cette dernière a terminé son exécution. Contrairement à une fonction classique qui retourne une valeur simple, la fermeture renvoie l'objet fonction lui-même, encapsulant son environnement local.
Les principales caractéristiques d'une fermeture sont :
- La capacité de mémoriser l'état des variables locales de la fonction parente.
- Une amélioration de la modularité du code en évitant l'usage excessif de variables globales.
- Une gestion spécifique de la mémoire, car les variables capturées ne sont pas détruites tant que la fermeture est référencée.
def generateur_prefixe(prefixe):
# Cette variable est capturée par la fonction interne
message_base = "Message: "
def formater(texte):
# Utilisation de nonlocal pour modifier une variable parente si nécessaire
nonlocal message_base
return f"{message_base} [{prefixe}] {texte}"
return formater
# Instance de fermeture
log_info = generateur_prefixe("INFO")
print(log_info.__name__) # Affiche: formater
print(log_info("Démarrage du système"))
# Résultat: Message: [INFO] Démarrage du système
Les fremetures sont particulièrement utiles dans la programmation asynchrone ou pour créer des décorateurs personnalisés, car elles permettent de "figer" des paramètres lors de la création de la fonction de rappel (callback).
Listes en compréhension vs Expressions génératrices
La distinction syntaxique majeure réside dans l'utilisation des crochets [] pour les listes et des parenthèses () pour les générateurs. Cependant, leur comportement interne, notamment concernant l'évaluation des variables, diffère radicalement.
Le piège de l'évaluation tardive (Late Binding)
Considérons l'exemple suivant utilisant une liste en compréhension contenant des fonctions anonymes (lambda) :
def creer_multiplicateurs():
# Liste en compréhension
return [lambda x: x * i for i in range(4)]
fonctions = creer_multiplicateurs()
resultats = [f(10) for f in fonctions]
print(resultats)
# Résultat: [30, 30, 30, 30]
Dans ce cas, la boucle for à l'intérieur de la liste se termine avant que les fonctions lambda ne soient appelées. À la fin de la boucle, la variable i vaut 3. Toutes les fonctions lambda pointent vers la même référence de i, produisent ainsi le même résultat (10 * 3).
L'avantage de l'expression génératrice
Si l'on remplace les crochets par des parenthèses, le comportement change en raison de l'évaluation paresseuse (lazy evaluation) :
def generer_multiplicateurs():
# Expression génératrice
return (lambda x: x * i for i in range(4))
generateur = generer_multiplicateurs()
resultats = [f(10) for f in generateur]
print(resultats)
# Résultat: [0, 10, 20, 30]
Ici, le générateur produit chaque fonction lambda une par une. À chaque itération du consommateur (la liste finale), le générateur avance d'un pas, capturant la valeur actuelle de i au moment de la génération de l'élément.
Générateurs et le mot-clé yield
Un générateur est une fonction spéciale qui utilise yield au lieu de return. Lorsqu'elle est appelée, elle ne renvoie pas une valeur immédiatement mais un objet générateur qui peut être parcouru.
def suite_fibonacci(limite):
a, b = 0, 1
compteur = 0
while compteur < limite:
yield a
a, b = b, a + b
compteur += 1
# Consommation du générateur
for nombre in suite_fibonacci(6):
print(nombre)
Itérateurs et Itérables
Pour bien structurer son code en Python, il est crucial de distinguer ces trois concepts :
- Itérable : Un objet disposant d'une méthode
__iter__capable de retourner un itérateur (ex: listes, dictionnaires). - Itérateur : Un objet possédant les méthodes
__iter__(retourne soi-même) et__next__(retourne l'élément suivant ou lèveStopIteration). - Générateur : Un itérateur simplifié créé via une fonction avec
yieldou une expression génératrice.