5.4. Les fermetures et les décorateurs¶
Nous complétons ce chapitre par la description de deux fonctionnalités avancées de Python pour la programmation fonctionnelle : les fermetures et les décorateurs.
5.4.1. Les fermetures¶
Une fermeture (« closure » en anglais) est une fonction définie dans le corps d’une autre.
Elle n’existe alors que dans le contexte de cette fonction, mais peut être renvoyée comme valeur de retour.
La fonction d’ordre supérieur se termine avec l’instruction return
mais son contexte (variables locales, paramètres, etc.) est toujours accessible à la fermeture par après.
La fermeture ainsi renvoyée possède alors une sorte d’état caché sans avoir à employer de variables globales.
def add(increment): # fonction d'ordre supérieur
def add_increment(n): # fermeture
return n + increment
return add_increment # on renvoie la fermeture qui a mémorisé la valeur de `increment`
# on crée une fonction `add_10()` en appelant `add()` avec 10 en paramètre
add_10 = add(10)
print(add_10(40))
# on peut sauter un étape en ne nommant pas la fermeture et en l'appelant directement
print(add(10)(40))
50
50
Note
Le processus de transformation d’une fonction du type add(x, y)
en add(x)(y)
se nomme la curryfication, en hommage au mathématicien Haskell Curry.
Grâce aux fonctions lambda, il est d’ailleurs possible de considérablement réduire le code de la fonction add()
ci-dessus :
def add(increment):
return lambda n: n + increment
5.4.2. Les décorateurs¶
Les décorateurs sont des fonctions d’ordre supérieur qui reçoivent une fonction en paramètre, l’enrobent dans une fermeture qui peut modifier ses paramètres ou ses valeurs de retour, lui ajouter des fonctionnalités de débogage, etc., puis qui renvoient cette fermeture. C’est alors la fermeture qui sera appelée au lieu de la fonction d’origine.
import time
import math
def timer_decorator(f): # le décorateur prend la fonction à décorer en paramètre
def inner(*kargs, **kwargs): # fermeture (`kargs`, `kwargs` : paramètres destinés à `f()`)
start = time.time()
result = f(*kargs, **kwargs) # on appelle `f()` avec l'ensemble de ses paramètres
stop = time.time()
print(stop - start) # temps écoulé pendant l'exécution de `f()`
return result # on renvoie le résultat de `f()` comme si on l'avait directement appelée
return inner # on renvoie la fermeture
def find_factors(n): # générateur pour rechercher tous les facteurs du paramètre `n`
for x in range(1, int(math.sqrt(n)) + 1):
if n % x == 0:
yield x, n // x
# on décore la fonction `list()` pour créer une version chronométrée
timed_list = timer_decorator(list)
# on appelle `timed_list()` pour obtenir toutes les valeurs du générateur `find_factors()`
print(timed_list(find_factors(746151943198651)))
3.4124951362609863
[(1, 746151943198651), (11, 67831994836241), (1718699, 434137649), (18905689, 39467059)]
La fonction affiche son temps d’exécution avant d’afficher les facteurs trouvés. Le chronométrage est transparent pour la fonction à laquelle il s’applique et n’affecte pas le déroulement du reste du programme.
Il est également possible de remplacer une fonction par sa version décorée après déclaration, par exemple en ajoutant list = timer_decorator(list)
dans le programme ci-dessus.
La version originale ne sera donc plus directement accessible.
Si nous souhaitons qu’une fonction soit automatiquement décorée, il est possible de l’indiquer à Python juste avant la déclaration de celle-ci à l’aide du symbole @
suivi du nom du décorateur à utiliser.
Utilisons cette syntaxe pour décorer deux fonctions dont le but est de déterminer si un nombre est premier.
Nous pourrons ainsi observer la différence entre la fonction naïve qui teste tous les diviseurs potentiels du nombre jusqu’à celui-ci et la version optimisée qui ne cherche pas de diviseurs supérieurs à sa racine carrée.
import time
import math
def timer_decorator(f):
def inner(*kargs, **kwargs):
start = time.time()
result = f(*kargs, **kwargs)
stop = time.time()
print(stop - start)
return result
return inner
@timer_decorator
def slow_is_prime(n):
for x in range(2, n):
if n % x == 0:
return False
return True
@timer_decorator
def faster_is_prime(n):
for x in range(2, int(math.sqrt(n)) + 1):
if n % x == 0:
return False
return True
NUMBER = 251567051
print(slow_is_prime(NUMBER))
print(faster_is_prime(NUMBER))
20.57187819480896
True
0.0015909671783447266
True
En les préfixant par @timer_decorator
, les fonctions slow_is_prime()
et faster_is_prime()
sont automatiquement décorées.
On peut constater que le gain en temps d’exécution est conséquent et l’écart ne fait que croître avec la grandeur du nombre premier.