Qu’est-ce qu’une closure en Python et Javascript ?


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 !

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

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 !'

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

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

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 !'

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"

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'

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

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

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'

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.

17 thoughts on “Qu’est-ce qu’une closure en Python et Javascript ?

  • JM

    À noter que pour modifier une variable à l’extérieur du scope de la fonction, on peut utiliser le mot de clé global qui permet d’avoir le même comportement qu’en Javascript.

    Encore moi pour la grammaire (supprime cette partie du commentaire, mais peut-être que c’est cool de laisser la partie ci-dessus.)

    Les closure (+s)
    on définie (t)
    ai (+t) été changé.
    ne créé pas de closure. (crée)

    • Sam Post author

      @JM: pas exactement.

      global permet de modifier une variable comme si c’était une variable globale. Il y a des effets de bords possibles (ce qui est un des gros défaut de Javascript) car une variable de tel nom peut exister à la racine du fichier.

      Une solution plus propre est d’utiliser le mot clé nonlocal, qui permet de considérer une variable comme étant celle du scope du dessus le plus proche. On évite ainsi de modifier en cascade

      Mais nonlocal n’existe qu’à partir de Python 3 que personne n’utilise en production.

      Merci pour les corrections :-) Plus un article est propre, plus il est agréable à lire. Surtout n’hésitez pas à faire le grammar nazi en commentaire, ce sera toujours bien acceuilli.

  • outsmirkable

    Cet article est complètement faux. Il y a confusion entre variable globale et closure. Une closure n’est pas une variable, c’est une FONCTION. En fait, créer une closure implique d’avoir une fonction qui retourne une fonction (la closure). Voici un exemple simple à retenir :

    »» def generer_closure(piegee):
    .       def closure(x):
    .            # Faire une opération avec la variable piégée
    .            return piegee ** x
    .       return closure
     
    »» # On génère une fonction qui "piège" la valeur 10
    »» f = generer_closure(10)
    »» f(3)
    1000
    »» # La fonction f se "souvient" de la valeur 10 !
    »» # On peut même supprimer le générateur
    »» del generer_closure
    »» f(2)
    100
    »» # Et ça marche toujours !
    »» # En fait notre fonction possède un attribut caché
    »» # Voilà où se cache la valeur piégée
    »» f.__closure__[0].cell_contents
    10

    Il arrive qu’on ait besoin d’écrire une méthode pour laquelle une partie des arguments ne change jamais. On est alors souvent tenté d’utiliser des variables globales pour ces valeurs “fixes”. Le but des closures est justement d’éviter de déclarer des variables globales, tout en obtenant une méthode agréable à utiliser, qui “connait” les valeurs fixes qu’elle doit utiliser.

  • Sam Post author

    Cher ami, une variable ne peut être globale que si elle est déclarée avec le mot clé global en Python, ou si elle n’est pas déclarée avec var en javascript. Ce n’est pas le cas dans l’article.

    La closure est un espace de mémoire reservé sous certaines conditions. Elle peut se faire dans le cadre de ton exemple – un retour de fonction – mais ce n’est pas le seul cas. On retrouve des closures dans tout un tas de blocs, dans différentes langages.

  • outsmirkable

    Cher Sam,

    Dans l’esprit « d’apprendre à un homme à pêcher », voici quelques commandes que vous pouvez taper sur votre console Python, je pense que ça vous éclaircira.

    Commençons par déclarer une variable :

    »»» x = 5


    Et maintenant une fonction l’utilisant :

    »»» def f():
    .       print x


    Quand nous faisons appel à f(), elle se « souvient » de x, même si x a été déclaré en-dehors de son scope.

    »»» f()
    5


    Jusqu’ici, tout va bien… Soyons fous, créons une deuxième fonction !

    »»» def g():
    .       print x + 1
    »»»
    »»» g()
    6


    Youpi, g() se “souvient” aussi de x ! Essayons de changer x.

    »»» x = 10
    »»» f()
    10
    »»» g()
    11


    f() et g() ont pris en compte la nouvelle valeur. Euh… on parlait pas de valeurs « piégées » ? C’est vraiment ça une closure ? Hmm… il n’y a pas d’objet __closure__ dans f, ni dans g…

    »»» f().__closure__
    »»»
    »»» g().__closure__
    »»»


    Que se passe-t-il si on efface la variable x ?

    »»» del x
    »»» x
    Traceback (most recent call last):
      File "", line 1, in 
    NameError: name 'x' is not defined
    »»»


    Ok, x n’est plus disponible, mais est-ce que f s’en souvient quand même ?

    »»» f()
    Traceback (most recent call last):
      File "", line 1, in 
      File "", line 2, in f
    NameError: global name 'x' is not defined


    Ah… bah non en fait. En plus Python nous parle d’un « global name ‘x’ » ? Il cherche une variable globale appelée x ? Je croyais que c’était seulement si je la déclarais “global x” ?!??!

    Pouf, pouf… essayons donc comme ça :

    »»» def generateur(y):
    .       def cloture():
    .           print y
    .       return cloture


    On va générer deux nouvelles fonctions, h et m, qui vont piéger des variables.

    »»» z = 5
    »»» h = generateur(z)
    »»» z = 10
    »»» m = generateur(z)
    »»» h()
    5
    »»» m()
    10


    Cool, j’ai deux fonctions très utiles (hem…) qui utilisent des valeurs que je n’ai pas besoin de leur passer en paramètre. Et f() a bien gardé la valeur 5, alors que j’ai changé la valeur de z entre-temps ! C’est bien, ça m’a l’air plus sécurisé… je ne voudrais pas que le comportement de ma fonction f change au bon vouloir des variables extérieures ! Il paraît que c’est pas bien. Et d’ailleurs même si je supprime la variable z, et mon générateur aussi :

    »»» del z
    »»» del generateur
    »»» h()
    5
    »»» m()
    10


    Waou, mes deux fonctions marchent toujours, ce sont des vraies fermetures ! On le voit bien d’ailleurs :

    »»» h.__closure__
    (,)

    Pour finir : http://fr.wikipedia.org/wiki/Fermeture_%28informatique%29

  • Sam Post author

    Effectivement, j’ai tort. Il va falloir que je mette à jour cet article.

  • Thibault

    Salut

    Merci Sam pour ton site, sur lequel on peut apprendre des trucs un peu pointu…

    Par contre, il m’a fallu lire TOUS les commentaires pour avoir la confirmation qu’en fait l’article était faux..
    Pourrais-tu mettre un petit commentaire (genre “la suite de cet article n’est pas vraiment exact… Vous aurez une mise à jour de l’article un peu plus tard quand j’aurai le temps…”) au début de l’article, bien visible (genre en rouge), pour montrer que tout dans cet article ne doit pas être pris comme une référence…

    Encore merci (ainsi qu’à outsmirkable), je sais maintenant ce qu’est une fermeture en python… :-)

    Thibault

  • sobriquet

    Il n’y a pas un petit problème d’indentation dans l’exemple de closure ?

  • Behold

    Salut,

    Malgré tous les efforts que tu as consacrés à la refonte totale de cet article, je remarque encore une petite erreur qui traîne dans ton avant-dernier exemple en Python:

    sonde = creer_sondage(“Schwarzenegger”)

    sonder(5)

    ‘Schwarzenegger a 5% intentions de vote’

    Il fallait bien sûr comprendre:

    sonde = creer_sondage(“Schwarzenegger”)

    sonde(5)

    ‘Schwarzenegger a 5% intentions de vote’

    (Sur la deuxième ligne, c’est ‘sonde’ et pas ‘sonder’. Ça vaut aussi pour les deux exemples après).

    sonder (celui avec un ‘r’ à la fin) est la fonction définie au sein de creer_sondage, donc, appelée en tant que telle par son petit nom, hors du contexte de la fonction, elle n’existe pas et c’est la voie de garage :

    NameError: name ‘sonder’ is not defined

    Vu que, par un bricolage infâme et/ou machiavélique voulu par le concepteur de l’article, cette fonction est aussi la valeur de retour de la fonction creer_sondage, elle est donc récupérable par la variable crée un peu plus tard lors de la commande:

    sonde = creer_sondage(“Schwarzenegger”)

    Donc il faut bien faire sonde(5), sonde(10), sonde(-2). CQFD.

    Sinon, je remarque que le concept me paraît très similaire à celui des fonctions statiques en C : une variable dans une fonction, qu’on peut ressusciter à chaque appel avec valeur précédente toujours en mémoire.

    D’ailleurs, ça et la possibilité d’instancier (ce que tu fais bien ici avec la variable sonde sans ‘r’ à la fin), ça ferait presque de la POO… mais tu as déjà souligné qu’après tout, la POO n’est qu’une manière d”habiller” des possibilités déjà offertes d’autres manières.

    Merci en tout cas pour tes nombreux articles et pour le temps que tu y consacres !

  • Sam Post author

    C’est de ma faute, j’ai voulu publier trop tôt : j’étais presser de sortir mais je voulais aussi publier l’article, et paf, erreur de précipitation !

  • Anne Onyme

    Comme pour les autres dépoussiérages, voici les quelques erreurs que j’ai repérées:

    * “et la c’est magic time !” -> “et là c’est magic time !”;

    * “passage de paramètre” -> “passage d’un argument” (cf. http://sametmax.com/la-difference-entre-parametres-et-arguments/);

    * “aux intention de vote” -> “aux intentions de vote”;

    * “ou modifier les paramètres qu’une fonction attend” -> “ou modifier les arguments qu’une fonction attend”.

  • toub

    J’avais bien compris la nouvelle version de l’article, jusqu’à tomber sur les commentaires, et là je pige plus rien. Finalement une closure, c’est une fonction ou un espace mémoire attaché à une fonction ?

  • Sam Post author

    Dans l’API de python, c’est exposé comme un attribut des fonctions, donc littéralement comme un espace mémoire attaché à une fonction.

  • Mik

    Hello,

    C’est pas plus simple d’utiliser les générateurs?
    Qu’est ce que ça apporte de plus?

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.