Sur le Web, il n'est pas rare de voir des exemples explicatifs de ce que sont les closures, mais plus rarement des exemples d'application des closures à des cas concrets. Je vais, dans plusieurs billets de ce blog, essayer de vous fournir un aperçu de cas concrets où l'utilisation des closures est intéressante.
Avant de commencer, il est bien sur nécessaire de clarifier ce qu'on entend par closure.
Au départ étaient les mathématiques et tout était clair et non-ambigüe. Ils donnèrent au nom de 'fermeture' (closure)une définition algébrique : Un ensemble d'éléments est dit fermé (closed) sous une certaine opération si l'application de cette opération aux éléments de l'ensemble produit un élément qui est encore un élément de l'ensemble.
Puis vinrent les informaticiens qui par leur nature fainéante, firent des raccourcis honteux : la communauté Lisp employa le mot closure pour décrire un concept complètement différent : une closure est une technique d'implémentation pour représenter des procédures ayant des variables libres.
De cette non-conformité au concept mathématique provient une partie de l'incompréhension qu'ont la plupart des programmeurs de base. Et puisqu'il faut rester concret quand on fait de l'informatique, voici l'exemple de base pour créer une closure, qu'on retrouve dans tous les articles d'introduction. L'exemple est en Python 2.7 :
def multiplier(nb):Ici, multi est la closure, multiplier est la fonction fabricant la closure. La plupart du temps, il s'agit d'encapsuler dans une fonction la création de la closure. La closure dispose alors des variables locales de la fonction dans laquelle elle a été créée. On peut appeler multiplier, l'usine à closures ('closure factory').
def multi(value):
return value * nb
return multi
On peut utiliser cette closure de cette façon :
>>> multi10 = multiplier(10)C'est un peut comme si on appelait l'usine à fabriquer des closures pour configurer au moment de l'exécution (runtime) une fonction particulière et d'en produire donc de toute sorte.
>>> multi10(2)
20
Comme il s'agit ici d'encloisonner des variables dans un scope particulier à la closure, on peut très bien définir le même comportement en utilisant la notion de classe classique. L'exemple ci-dessous converti en classe donne :
class MultiplierFactory:
def __init__(self, nb):
self.nb = nb
def multiply(self, value):
return self.nb * value
>>> multi10 = MultiplierFactory(10).multiply
>>> multi10(2)
20
C'est une façon de créer des pseudo-classes anonymes, sachant qu'une classe ne sert qu'à gérer la 'globalité' de certaines variables. Pour continuer le parallèle entre une closure et une classe, il va être possible, de façon beaucoup plus succincte d'exprimer des modèles de conception comportementaux en closure.
Une closure n'est pas simplement une fonction anonyme qu'on se passe de variable en variable. Pour réellement parler de closure, il faut qu'il y ait en jeu des variables libres telles que nb dans l'exemple précédent.
A noter aussi qu'une closure ne s'exécute qu'au dernier moment, lors de son appel explicite. La configuration de la fonction n'a pas nécessité de faire fonctionner du code source de cette fonction.
C'est par exemple, intéressant quand on veut programmer par évènements et appels en retour (callbacks). Par exemple, sans closure, on pourrait essayer de coder quelque chose comme ça, mais ce code ne peut pas marcher :
from threading import TimerEn effet, Timer(timeout, show_message(msg)) va appeler show_message(msg) avant d'appeler Timer. Timer va donc être appelé avec le résultat de l'appel à show_message(msg), ce qui n'est pas ce que l'on veut. Par contre, avec une closure, on peut écrire le code de cette manière :
def show_message(msg):
print msg
def set_alarm(msg, timeout):
Timer(timeout, show_message(msg))
>>> set_alarm("Réveille Toi !", 3)
Réveille Toi ! ---> s'affiche instantanément
from threading import TimerCe bout de code permet d'éviter l'emploi d'une variable globale msg, qu'il serait alors difficile de maintenir cohérente dans un environnement multithread par exemple.
def set_alarm(msg, timeout):
def show_message():
print msg
t = Timer(timeout, show_message)
t.start()
>>> set_alarm("Réveille Toi !", 3)
Au bout de 3 secondes on a :
>>> Réveille Toi !