Une closure en Python : la fonction qui se souvient de son contexte

Une closure retient les variables de la fonction où elle est née. À quoi elle sert, ce qu'elle capture vraiment, et le piège de la boucle.
6 min de lecture
Believemy logo

Définition

Une fonction ordinaire oublie tout entre deux appels : ses variables locales disparaissent dès qu'elle rend son résultat. L'ennui commence quand on veut livrer une fonction déjà réglée, une qui multiplie par deux et une autre par trois, sans recopier le même corps pour un seul chiffre qui change.

Une closure est la réponse de Python à ce besoin. C'est une fonction définie à l'intérieur d'une autre, et qui se souvient des variables de la fonction englobante longtemps après que celle-ci a rendu la main. Le mot anglais signifie « fermeture » : la fonction interne se referme sur son contexte de naissance et l'emporte avec elle.

Voici le moule, et les deux fonctions qui en sortent.

PYTHON
def fabriquer_multiplicateur(facteur):
    def multiplier(nombre):
        return nombre * facteur  # facteur appartient à la fonction du dessus

    return multiplier  # on renvoie la fonction elle-même, pas son résultat

doubler = fabriquer_multiplicateur(2)
tripler = fabriquer_multiplicateur(3)

print(doubler(10))  # 20
print(tripler(10))  # 30

fabriquer_multiplicateur s'est terminée deux fois, et pourtant facteur est toujours là, en deux exemplaires indépendants : une variable locale aurait disparu, celle-ci survit parce qu'une fonction interne la lit encore.

Ce que cela change à l'écriture : vous livrez une fonction déjà configurée au lieu de traîner ses réglages en argument partout. doubler s'utilise comme une fonction à un seul paramètre.


Les trois conditions

Une closure ne se déclare pas, elle se constate : aucun mot-clé ne l'annonce, c'est la forme du code qui la fabrique. Reste à savoir la reconnaître. Trois éléments doivent être réunis, et un seul qui manque laisse une fonction imbriquée banale.

ConditionCe qu'elle veut dire
Une fonction imbriquéeUn def écrit à l'intérieur d'un autre
Une variable libreLa fonction interne lit un nom qui ne lui appartient pas et qui n'est pas global
Une sortie du nidLa fonction interne est renvoyée par return ou rangée quelque part

La troisième est celle qu'on oublie le plus souvent : tant que la fonction interne reste enfermée dans celle du dessus, elle meurt avec elle et le mécanisme ne sert à rien.

Bon à savoir

Python garde la trace de cette capture dans un attribut __closure__, posé sur la fonction interne et valant None quand il n'y a rien à retenir. doubler.__closure__[0].cell_contents renvoie 2 : de quoi vérifier qu'une closure s'est formée, au lieu de le supposer.


Elle capture la variable, pas sa valeur

Voici le piège qui coûte le plus de temps. Une closure retient un nom et l'emplacement où ce nom est rangé, pas une photographie de son contenu : elle ira relire cet emplacement au moment de l'appel, pas une seconde plus tôt.

Trois fonctions créées dans une boucle, censées renvoyer 0, 1 et 2.

PYTHON
fonctions = []

for i in range(3):
    fonctions.append(lambda: i)  # i n'est pas lu ici, seulement retenu

print([f() for f in fonctions])  # [2, 2, 2] au lieu de [0, 1, 2]

Les trois lambda partagent la même portée et donc le même i, qui vaut 2 une fois la boucle terminée. Aucune ne l'a lu pendant la boucle : elles le lisent toutes à l'appel, beaucoup trop tard.

Attention

Cette erreur ne lève aucune exception. Le programme tourne, les fonctions répondent, et les valeurs sont fausses. C'est ce silence qui la rend chère : on la découvre bien plus loin, dans un résultat qui ne colle pas.

La correction consiste à figer la valeur à la création, avec un argument par défaut : il est évalué une seule fois, à la définition, et ce qui est capturé là devient une copie indépendante de la suite de la boucle.

PYTHON
fonctions = []

for i in range(3):
    fonctions.append(lambda valeur=i: valeur)  # valeur=i est évalué tout de suite

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


Modifier ce qui a été capturé

Lire une variable capturée ne demande rien à personne. L'affecter est une autre histoire : dès qu'une fonction interne écrit dans un nom, Python le considère comme local du début à la fin, et l'appel se solde par une UnboundLocalError.

Le mot-clé nonlocal lève l'ambiguïté en désignant explicitement la variable de la fonction du dessus. On obtient un compteur qui garde son total d'un appel à l'autre.

PYTHON
def compteur():
    total = 0

    def incrementer():
        nonlocal total  # total désigne celui du dessus, pas un nouveau local
        total += 1
        return total

    return incrementer

suivant = compteur()
print(suivant(), suivant(), suivant())  # 1 2 3

À ne pas confondre avec global, qui vise le niveau du module et non la fonction englobante. Chaque appel à compteur fabrique un compteur neuf avec son propre total, là où une globale serait partagée par tout le programme.


Closure ou classe ?

Une closure et un objet résolvent le même problème : associer un comportement à un état qui persiste entre les appels. Le choix se tranche sur la quantité d'état à conserver et le nombre d'opérations à offrir.

CritèreClosureClasse
Nombre d'opérationsUne seulePlusieurs
État à conserverUne ou deux valeursAutant qu'il faut
Lisibilité de l'étatImplicite, à devinerExplicite, nommé
ÉcritureDeux fonctions imbriquéesUn class et ses méthodes

La closure gagne quand il n'y a qu'une chose à faire et une ou deux valeurs à retenir. Dès qu'un troisième comportement apparaît, la classe redevient plus claire : l'état y porte un nom visible au lieu de vivre caché dans une portée intermédiaire.


Questions fréquentes

Question

À quoi sert une closure dans du vrai code ?

À fabriquer des fonctions sur mesure : un validateur réglé sur la longueur attendue, une fonction de tri configurée à la volée, un cache de résultats. C'est aussi le mécanisme sur lequel repose tout décorateur, une closure enveloppant la fonction reçue.

Question

Une closure consomme-t-elle de la mémoire ?

Oui, et c'est le point à surveiller. Tant que la fonction interne existe quelque part, les variables qu'elle a capturées ne peuvent pas être libérées : un gros tableau capturé dans une closure rangée dans un registre est retenu pour toute la durée du programme.

Question

Faut-il maîtriser les closures pour bien programmer en Python ?

Elles ne sont pas indispensables au début. Elles expliquent en revanche des choses employées tous les jours sans être comprises : les décorateurs, les fonctions de rappel, les usines à fonctions. Le sujet est repris pas à pas dans la formation Python.

Termes connexes

Découvrez notre glossaire Python

Parcourez les termes et définitions les plus couramment utilisés dans le domaine du développement avec Python.

Partager cet article

Tu veux nous aider ? Fais un lien vers cet article sur tes réseaux ou encore mieux : sur ton site, dans un article ou dans ta newsletter.