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.

En Python

Scope

Le scope d’une variable, aussi appelée sa portée, est la “zone” dans laquelle une variable est accessible.

Par exemple avec :

def sondage(candidat, intention) :
    marge = 5
    return "%s a %s%% d'intention de vote" % (candidat, intention)
 
print marge

On obtient:

UnboundLocalError: local variable 'marge' referenced before assignment

Car marge est définie et donc accessible dans sondage(), mais pas en dehors de sondage(). On dit que le scope de marge est limité au block de la fonction sondage().

Closure

Une closure est l’inclusion dans la définition d’une fonction d’une variable d’un scope supérieur.

Par exemple :

marge = 5
 
def sondage(candidat, intention):
    intention -= marge
    return "%s a %s%% d'intention de vote" % (candidat, intention)
 
print sondage('Schwarzenegger', 30)

Va afficher :

Schwarzenegger a 25% d'intention de vote

C’est une fonctionalité du langage : Python autorise une fonction à utiliser une variable, malgré le fait qu’elle ne soit pas définie à l’intérieur de la fonction.

On appelle cela closure car la référence de cette variable est “piégée” dans la fonction. Si on met à jour la variable, c’est pris en compte dans la fonction :

marge = 5
 
def sondage(candidat, intention):
    intention -= marge
    return "%s a %s%% d'intention de vote" % (candidat, intention)
 
print sondage('Schwarzenegger', 30)
 
marge = 10
 
print sondage('Schwarzenegger', 30)

Va afficher :

Schwarzenegger a 25% d'intention de vote
Schwarzenegger a 20% d'intention de vote

Car le deuxième print arrive après que marge ait été changé.

Pour éviter que des closures vous empêchent de choisir le nom de vos variables dans une fonction, si vous redéfinissez la variable de la closure avant de l’utiliser, Python ignore la closure et initialise la variable :

marge = 5
 
def sondage(candidat, intention):
    marge=0
    intention -= marge
    return "%s a %s%% d'intention de vote" % (candidat, intention)
 
print sondage('Schwarzenegger', 30)
 
def sondage(candidat, intention, marge=0):
    intention -= marge
    return "%s a %s%% d'intention de vote" % (candidat, intention)
 
print sondage('Schwarzenegger', 30)

Ce qui donne :

Schwarzenegger a 30% d'intention de vote
Schwarzenegger a 30% d'intention de vote

Ici marge est définie en tant qu’argument ou comme variable locale. Donc, Python utilise ce qu’il a sous la main, et ne crée pas de closure.

Attention !

Les closures sont en lecture seule. Si vous décidez de les utiliser, vous ne pouvez pas les modifier :

marge = 5
 
def sondage(candidat, intention):
    # ici on utilise la variable sans la définir
    # donc python créé la closure
    m = marge
    marge = m + 7
    intention -= marge
    return "%s a %s%% d'intention de vote" % (candidat, intention)
 
print sondage('Schwarzenegger', 30)

Ceci plante avec un message d’erreur pas très explicite:

UnboundLocalError: local variable 'marge' referenced before assignment

Plus vicieusement, ceci non plus ne marche pas:

marge = 5
def sondage(candidat, intention):
    # marge += 7 veut dire en fait marge = 7 + marge
    # et comme marge n'est pas définie python créé la closure !
    marge += 7
    intention -= marge
    return "%s a %s%% d'intention de vote" % (candidat, intention)
 
print sondage('Schwarzenegger', 30)

Bref, si vous voulez utiliser une closure et l’incrémenter, il faut creer une variable intermédiaire :

marge = 5
 
def sondage(candidat, intention):
    m = m + marge # on écrit pas dans marge, donc pas de problème
    intention -= m
    return "%s a %s%% d'intention de vote" % (candidat, intention)
 
print sondage('Schwarzenegger', 30)

En Javascript

Les closure en javascript marchent exactement de la même manière qu’en Python :

var marge = 5;
 
function sondage(candidat, intention){
    intention -= marge
    return candidat + " a " + intention + "% d'intention de vote";
}
 
console.log(sondage('Schwarzenegger', 30));

Output:

Schwarzenegger a 25% d'intention de vote

Mais il y a une différence notable !

Non seulement on peut modifier la closure mais en plus la valeur est aussi modifiée à l’extérieur de la fonction.

var marge = 5;
 
function sondage(candidat, intention){
    marge += 7;
    intention -= marge
    return candidat + " a " + intention + "% d'intention de vote";
}
 
console.log(sondage('Schwarzenegger', 30));
console.log(marge)
Schwarzenegger a 18% d'intention de vote
12

A quoi cela peut-il servir ?

C’est surtout utile quant on définit une fonction dans une fonction. Par exemple quand on créé un décorateur en Python. Ou quand on on passe la variable à un callback, par exemple avec jQuery:

var somme_de_trucs = 0;
$.each(liste_de_trucs, function(i, truc){
    somme_de_trucs += truc;
});
console.log(somme_de_trucs);

En effet $.each attend un callback en second parametre, et c’est grace à la closure qu’on peut modifier somme_de_trucs.

No related posts.

flattr this!

6 comments

  1. À 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)

  2. @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.

  3. 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.

  4. 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.

  5. 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

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

Flux RSS des commentaires

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">

Jouer à mario en attendant que les autres répondent