Cet article a été mis à jour et contient maintenant du code Python en version 3

Impossible de trouver une explication simple des closures sur le Net. Pourtant c’est un concept qui peut se comprendre en quelques minutes.

Diableries en Python

Si vous avez lu l’article sur les décorateurs, vous vous souvenez peut être qu’en Python, on peut faire des trucs chelous avec les fonctions.

Par exemple, on peut définir une fonction, dans une fonction :

>>> def une_fonction ( ) : ... def une_autre_fonction ( ) : ... return "hello !" ... print ( une_autre_fonction ( ) ) ... print ( 'salut !' ) >>> une_fonction ( ) hello ! salut ! >>> def une_fonction(): ... def une_autre_fonction(): ... return "hello !" ... print(une_autre_fonction()) ... print('salut !') >>> une_fonction() hello ! salut !

Comme cette fonction est définie dans une autre fonction, elle n’est pas accessible en dehors de cette fonction :

>>> une_autre_fonction ( ) --------------------------------------------------------------------------- NameError Traceback ( most recent call last ) < ipython-input- 14 -6955138255cd > in < module > ( ) ---- > 1 une_autre_fonction ( ) NameError : name 'une_autre_fonction' is not defined >>> une_autre_fonction() --------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-14-6955138255cd> in <module>() ----> 1 une_autre_fonction() NameError: name 'une_autre_fonction' is not defined

Mais on peut faire encore plus zarb. On peut définir une fonction, et … retourner la fonction.

Attention, pas le résultat de la fonction, la fonction elle-même !

>>> def faire_une_fonction ( ) : ... def une_fonction_toute_fraiche ( ) : ... return "so fresh !" ... ... # et la c'est magic time ! ... return une_fonction_toute_fraiche ... # pas de parenthèse à la fin. On n’appelle pas la fonction, ... # on retourne l'objet fonction. >>> fonction = faire_une_fonction ( ) >>> fonction ( ) 'so fresh !' >>> def faire_une_fonction(): ... def une_fonction_toute_fraiche(): ... return "so fresh !" ... ... # et la c'est magic time ! ... return une_fonction_toute_fraiche ... # pas de parenthèse à la fin. On n’appelle pas la fonction, ... # on retourne l'objet fonction. >>> fonction = faire_une_fonction() >>> fonction() 'so fresh !'

Donc en Python on peut créer une fonction à la volée, et la retourner. Pour les curieux, ça s’appelle une factory (une usine quoi).

C’est notamment, comme je le disais plus haut, le truc donc on se sert pour les décorateurs, mais ça permet aussi de créer du code qui contient des données préexistantes, au moment où l’on reçoit ces données.

C’est là qu’interviennent les closures.

Définition formelle et super sérieuse d’une closure

Si vous créez cette fonction :

>>> def faire_une_fonction ( ) : ... def une_fonction_toute_fraiche ( ) : ... return "so fresh !" ... return une_fonction_toute_fraiche >>> def faire_une_fonction(): ... def une_fonction_toute_fraiche(): ... return "so fresh !" ... return une_fonction_toute_fraiche

Mais qu’au lieu de faire ça, vous définissez le message dans une variable dans la fonction faire_une_fonction() :

>>> def faire_une_fonction ( ) : ... fraichitude = "so fresh !" ... def une_fonction_toute_fraiche ( ) : ... return fraichitude ... return une_fonction_toute_fraiche >>> def faire_une_fonction(): ... fraichitude = "so fresh !" ... def une_fonction_toute_fraiche(): ... return fraichitude ... return une_fonction_toute_fraiche

Pour que ça marche, une_fonction_toute_fraiche() doit avoir accès à la variable définie dans la fonction du dessus afin de pouvoir l’afficher. Mais comme on retourne la fonction, on sort de la portée de cette variable.

Pour pallier ce problème, un espace mémoire spécial est créé automatiquement par Python qui va stocker une référence à cette valeur DANS une_fonction_toute_fraiche() .

Cet espace mémoire est appelé la closure.

Python donne accès à cet espace mémoire :

>>> fonction = faire_une_fonction ( ) >>> fonction < function __main__ . une_fonction_toute_fraiche > >>> fonction.__closure__ ( < cell at 0x7f2052fb3f18 : str object at 0x7f204f2265f0 >, ) >>> fonction.__closure__ [ 0 ] . cell_contents 'so fresh !' >>> fonction = faire_une_fonction() >>> fonction <function __main__.une_fonction_toute_fraiche> >>> fonction.__closure__ (<cell at 0x7f2052fb3f18: str object at 0x7f204f2265f0>,) >>> fonction.__closure__[0].cell_contents 'so fresh !'

Comme vous le voyez, la valeur de la variable fraichitude est attachée à la fonction une_fonction_toute_fraiche .

Expliquons aussi à quoi ça sert

Parce que là, tout de suite, je suis certain que vous vous demandez pourquoi on voudrait un truc aussi tordu.

Et bien la raison principale, c’est que ça permet d’attacher un état à la fonction. Cet état peut être créé au niveau de la fonction factory, donc il n’a pas besoin d’être hard codé dans l’autre.

Ouai, je sens que ce n’est toujours pas clair. Un petit exemple ?

J’ai un sondage d’un candidat à la présidentielle. Je sauvegarde son nom et le pourcentage d’intentions de vote, puis je veux pouvoir afficher l’évolution de ces intentions de vote.

Puisqu’on a des états (le nom du candidat et le % d’intentions de vote), on pourrait très bien utiliser une classe et faire un objet (si ça ne vous parle pas, on a un guide POO :)) :

class Sondage: def __init__ ( self , candidat ) : # ici notre état est explicitement attaché # à self self . candidat = candidat self . intentions_de_vote = 0 def sonder ( self , valeur ) : msg = "{} a {}% d'intentions de vote" self . intentions_de_vote + = valeur return msg. format ( self . candidat , self . intentions_de_vote ) >>> sondage = Sondage ( "Schwarzenegger" ) >>> sondage. sonder ( 5 ) "Schwarzenegger a 5% d'intentions de vote" >>> sondage. sonder ( 10 ) "Schwarzenegger a 15% d'intentions de vote" >>> sondage. sonder ( - 3 ) "Schwarzenegger a 12% d'intentions de vote" class Sondage: def __init__(self, candidat): # ici notre état est explicitement attaché # à self self.candidat = candidat self.intentions_de_vote = 0 def sonder(self, valeur): msg = "{} a {}% d'intentions de vote" self.intentions_de_vote += valeur return msg.format(self.candidat, self.intentions_de_vote) >>> sondage = Sondage("Schwarzenegger") >>> sondage.sonder(5) "Schwarzenegger a 5% d'intentions de vote" >>> sondage.sonder(10) "Schwarzenegger a 15% d'intentions de vote" >>> sondage.sonder(-3) "Schwarzenegger a 12% d'intentions de vote"

Et ça marche parfaitement.

En fait, les closures nous permettent d’obtenir le même effet, mais avec une fonction :

>>> def creer_sondage ( candidat ) : ... intentions_de_vote = 0 ... ... def sonder ( valeur ) : ... # ici notre état est automatiquement ... # stocké dans une closure ... nonlocal intentions_de_vote ... intentions_de_vote + = valeur ... msg = "{} a {}% d'intentions de vote" ... return msg. format ( candidat , intentions_de_vote ) ... ... return sonder >>> sonder = creer_sondage ( "Schwarzenegger" ) >>> sonder ( 5 ) 'Schwarzenegger a 5% intentions de vote' >>> sonder ( 10 ) 'Schwarzenegger a 15% intentions de vote' >>> sonder ( - 2 ) 'Schwarzenegger a 13% intentions de vote' >>> def creer_sondage(candidat): ... intentions_de_vote = 0 ... ... def sonder(valeur): ... # ici notre état est automatiquement ... # stocké dans une closure ... nonlocal intentions_de_vote ... intentions_de_vote += valeur ... msg = "{} a {}% d'intentions de vote" ... return msg.format(candidat, intentions_de_vote) ... ... return sonder >>> sonder = creer_sondage("Schwarzenegger") >>> sonder(5) 'Schwarzenegger a 5% intentions de vote' >>> sonder(10) 'Schwarzenegger a 15% intentions de vote' >>> sonder(-2) 'Schwarzenegger a 13% intentions de vote'

Comment ce snippet marche-t-il ?

D’abord, nous avons creer_sondage qui est une factory, et qui nous fabrique puis retourne la fonction sonder . Mais cette dernière accède aux variables candidat et intentions_de_vote qui sont définies au-dessus. Un espace mémoire spécial est donc créé par Python pour donner accès à ces variables.

A chaque fois qu’on appelle sonder() , candidat et intentions_de_vote sont à leur valeur précédente, car elles sont stockées dans cet espace mémoire, et réaccédées :

>>> sonder.__closure__ ( < cell at 0x7f2052fc5fa8 : str object at 0x7f204f212d30 >, < cell at 0x7f2052fb37f8 : int object at 0x9f8980 > ) >>> sonder.__closure__ [ 0 ] . cell_contents 'Schwarzenegger' >>> sonder.__closure__ [ 1 ] . cell_contents 13 >>> sonder.__closure__ (<cell at 0x7f2052fc5fa8: str object at 0x7f204f212d30>, <cell at 0x7f2052fb37f8: int object at 0x9f8980>) >>> sonder.__closure__[0].cell_contents 'Schwarzenegger' >>> sonder.__closure__[1].cell_contents 13

Ce qui explique que quand on rajoute des pourcents aux intention de vote via le passage de paramètre à chaque appel, ils se cumulent dans la variable intentions_de_vote : c’est toujours la même variable qui est modifiée, car elle est piégée dans la closure.

Vous avez dû noter un mot-clé assez rarement utilisé :

nonlocal intentions_de_vote nonlocal intentions_de_vote

Pour faire simple, disons que les closures en Python sont en lectures seules, à moins qu’on précise explicitement avec nonlocal qu’on va utiliser une variable qui n’est pas locale et qu’on va la modifier.

C’est une contrainte spécifique à Python liée à la manière dont il gère la portée des variables, et c’est un peu relou. Bref, si vous voulez modifier la valeur d’une closure, il faut marquer la variable avec nonlocal .

En JavaScript

Les closures en JavaScript marchent de la même manière qu’en Python :

function creerSondage ( candidat ) { var intentions_de_vote = 0 ; // JS a des fonctions anonymes donc on peut // la retourner cash return function ( valeur ) { // poof, toutes les variables ici // sont dans une closures intentions_de_vote += valeur return candidat + "a " + intentions_de_vote + "% d'intentions de vote" } ; } var sonder = creerSondage ( "Schwarzenegger" ) console. log ( sonder ( 5 ) ) 'Schwarzenegger a 5% intentions de vote' console. log ( sonder ( 10 ) ) 'Schwarzenegger a 15% intentions de vote' console. log ( sonder ( - 2 ) ) 'Schwarzenegger a 13% intentions de vote' function creerSondage(candidat) { var intentions_de_vote = 0; // JS a des fonctions anonymes donc on peut // la retourner cash return function(valeur) { // poof, toutes les variables ici // sont dans une closures intentions_de_vote += valeur return candidat + "a " +intentions_de_vote + "% d'intentions de vote" }; } var sonder = creerSondage("Schwarzenegger") console.log(sonder(5)) 'Schwarzenegger a 5% intentions de vote' console.log(sonder(10)) 'Schwarzenegger a 15% intentions de vote' console.log(sonder(-2)) 'Schwarzenegger a 13% intentions de vote'

Et pas besoin de spécifier nonlocal .

On utilise beaucoup plus souvent les closures en JS qu’en Python. En Python, c’est surtout pour les décorateurs, mais en JS, comme on a des callbacks partout, il faut trouver un moyen de passer l’état du programme aux callbacks, et généralement on le fait via des closures.

Petit résumé ?

Une closure est un espace mémoire créé quand une fonction B est définie dans une fonction A, et que B accède à des variables définies dans A.

Il n’y a qu’un espace mémoire, attaché à B, qui est réutilisé à chaque appel de B, donc si les valeurs des variables changent, B aura accès aux nouvelles valeurs à chaque appel.

Cela est utile quand on veut partager un état entre plusieurs fonctions, sans utiliser la POO, des variables globales ou modifier les paramètres qu’une fonction attend. Cela permet par ailleurs d’initialiser cet état dynamiquement.