en guise de complément, ce notebook introduit la notion de decorator
déjà rencontré¶
on a déjà rencontré les décorateurs lorsqu’on a vu les méthodes statiques et les méthodes de classe
class Foo:
@staticmethod
def load_from_file(filename):
instance = Foo(...)
return instancedans cette notation, staticmethod est un décorateur
nous allons voir ça plus en détail
pourquoi faire ?¶
l’idée du décorateur, c’est de transformer une fonction pour en déduire une autre fonction, qui fait un peu plus de choses que la première
exemples
je veux afficher un message en entrant et en sortant de la fonction
je veux compter le nombre de fois qu’une fonction est appelée
je veux ‘cacher’ les résultats de la fonction que j’ai déjà calculés
...
principe¶
un décorateur, c’est donc un bidule qui transforme une fonction en une autre fonction
de quelle nature est ce bidule ? une fonction, bien sûr
exemple¶
essentiellement la même chose que sur la figure, avec quelques détails en plus:
comment gérer tous les paramètres (
*argset**kwds)comment trouver proprement le nom de la fonction
# pour débugger une fonction, c'est pratique
# de pouvoir afficher les arguments et le résultat
def decorator(foo):
def decorated(*args, **kwds):
print(f"IN {foo.__name__}({args=}, {kwds=})")
result = foo(*args, **kwds)
print(f"OUT {foo.__name__} -> {result}")
return result
return decorated# une fonction au hasard
def add(x, y):
"""
la somme
"""
return x + y# la version décorée
# affiche les paramètres et le résultat
add1 = decorator(add)
add1(10, 20)IN add(args=(10, 20), kwds={})
OUT add -> 30
30# on peut appliquer la même recette
# à n'importe quelle fonction
# une autre
def mul(x, y=10):
return x * y# la version décorée ... pareil
mul1 = decorator(mul)
mul1(10, y=20)IN mul(args=(10,), kwds={'y': 20})
OUT mul -> 200
200la syntaxe @truc¶
bien sûr en pratique on nomme les décorateurs de manière plus explicite
disons qu’on appelle le nôtre
print_in_out
au lieu d’écrire
def add(x, y):
return x + y
add = print_in_out(add)on peut se contenter de
@print_in_out
def add(x, y):
return x + yle module functools¶
il expose quelques décorateurs d’usage courant;
voyons un exemple d’utilisation du décorateur cache
# on part d'une implémentation super inefficace
# de la suite de fibonacci
def fibo(n):
return 1 if n <= 1 else fibo(n-1) + fibo(n-2)# la preuve
%timeit -n 1 -r 1 fibo(32)1.27 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
# avec ce décorateur, la fonction va retenir les
# calculs qu'elle a faits précédemment
from functools import cache
@cache
def fibo(n):
return 1 if n <= 1 else fibo(n-1) + fibo(n-2)# et du coup ça va plusieurs ordres de grandeur fois plus vite !
%timeit -n 1 -r 1 fibo(30)15.4 μs ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
décorateur de classes¶
en fait le même principe s’applique aux classes; on peut concevoir une fonction qui prend en paramètre un objet classe et le transforme en une autre classe
et en fait on en a aussi déjà vu un exemple, je vous renvoie à la section sur les dataclasses
conclusion¶
un décorateur est un callable (par exemple une fonction) qui transforme une fonction en une version instrumentée de la fonction (ou une classe en une classe)
avec la syntaxe
@bidule def ma_fonction(...): ...on peut remplacer dans tout le code chaque appel à
ma_fonctionpar un appel équivalent à la fonction décorée parbidulesachez aussi que les usages avancés des décorateurs permettent de passer des paramètres ... au décorateur lui-même; un sujet que je vous laisse creuser si vous êtes intéressé
les attributs de fonction (avancé)¶
une fonction est un objet Python
sur lequel on peut définir des attributs arbitraires
et qui possède de base des attributs spéciaux
# quand on utilise `def`
# on a gratuitement le nom
add.__name__'add'# et le docstring
add.__doc__'\n la somme\n 'préserver les attributs spéciaux¶
du coup, notre première implémentation est améliorable car
# ce n'est pas très parlant
# on aimerait avoir ici 'add'
add1.__name__'decorated'# pareil ici, on a perdu la docstring, c'est vide
add1.__doc__# du coup on pourrait écrire quelque chose comme ceci
# (mais voyez le slide suivant pour la 'bonne' façon de faire)
def decorator2(f):
def decorated(*args, **kwds):
print(f"IN {f.__name__}({args=}, {kwds=})")
result = f(*args, **kwds)
print(f"OUT {f.__name__}")
return result
decorated.__name__ = f.__name__
decorated.__doc__ = f.__doc__
return decoratedadd2 = decorator2(add)
add2.__name__, add2.__doc__('add', '\n la somme\n ')préserver les attributs spéciaux (2)¶
ou encore, la méthode recommandée est d’utiliser .. le décorateur wraps
qui va faire tout ce travail pour nous, avec une simple ligne en plus par rapport à la version naïve
from functools import wraps
def decorator3(f):
@wraps(f)
def decorated(*args, **kwds):
print(f"IN {f.__name__}({args=}, {kwds=})")
result = f(*args, **kwds)
print(f"OUT {f.__name__}")
return result
return decorated# et maintenant on a bien tout comme on voulait
add3 = decorator3(add)
add3.__name__, add3.__doc__('add', '\n la somme\n ')les callables (avancé)¶
ce sujet nous donne l’occasion de parler des callables
en réalité, un decorator n’est pas nécessairement une fonction; la vraie contrainte est qu’on puisse l’appeler, en lui passant en argument une fonction en paramètre
et parmi les protocoles disponibles sur les classes Python, il y a la notion de callable, qui dicteque si une classe définit la méthode spéciale __call__, alors on peut utiliser ses instances comme une fonction - on peut donc appeler l’instance, d’où le terme de callable
un exemple simple¶
petite digression, juste pour bien voir cette notion de callable d’abord sur un exemple relativement simple
imaginons qu’on a écrit une classe Line
qui possède comme attributs les termes et de l’équation
du coup chaque instance de cette classe peut être aussi vue comme une fonction qui a x fait correspondre le y calculé de cette façon
c’est très simple d’implémenter cette idée en Python
# pour le fun on va utiliser une dataclasse
# ça nous fera économiser le boiler plate usuel
# en plus ça nous permet de revoir ce concept
# qui n'est rien d'autre que .. un décorateur de classes
from dataclasses import dataclass
class Line:
def __init__(self, a, b):
self.a = a
self.b = b
# pour rendre nos objets callables
def __call__(self, x):
return self.a * x + self.b# la droite y = 3x + 1
line = Line(3, 1)
# et cet objet peut être utilisé comme une fonction !
# pour x=2 on doit obtenir 3*2+1 = 7
line(2)7un décorateur comme une classe¶
du coup on peut tirer profit de ce trait pour implémenter notre décorateur, non plus comme une fonction mais comme une classe
pourquoi faire ça me direz-vous ?
eh bien parce que cela permet de tirer profit de l’avantage que procure une classe: les objets retiennent un état, ce qui n’est pas le cas des fonctions
pour le fun, voyons comment on pourrait implémenter le décorateur de cache de cette façon
# to keep it simple, we support only positional arguments
# also the parameters must be hashable
# usually a decorator's name would be lowercase
# however we use CamelCase here to outline the fact
# that it is implemented as a class
class Cache:
def __init__(self, f):
self.f = f
self.cached_values = dict()
def __call__(self, *args):
#
def decorated(*args):
if args in self.cached_values:
return self.cached_values[args]
else:
result = self.f(*args)
self.cached_values[args] = result
return result
return decorated# and with this in place we can use Cache as a decorator
@Cache
def fibo(n):
if n <= 1:
return 1
else:
return fibo(n-1) + fibo(n-2)# and it is as efficient as expected
%timeit -n 1 -r 1 fibo(300)3.31 μs ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
comment ça marche ?¶
on applique simplement les principes qu’on a vus jusqu’ici:
on calcule
Cache(fibo)qui se trouve être un objet callable car
Cacheimplémente__call__()et c’est cet objet qui est appelé lorsqu’on écrit
fibo(300)
épilogue¶
en élaborant sur ce principe on peut écrire toutes les combinaisons, i.e.:
sous la forme d’une fonction un décorateur de fonction ou de classe
sous la forme d’une classe un décorateur de fonction ou de classe
quelques exemples¶
voici pour finir quelques exemples de décorateurs un peu plus réalistes
pour montrer aussi qu’on peut sans souci chainer les décorateurs...
from functools import wraps
def runtime(func):
"""
Décorateur qui affiche le temps d'exécution d'une fonction
"""
import time
@wraps(func)
def wrapper(*args, **kwargs):
t = time.perf_counter()
res = func(*args, **kwargs)
print(func.__name__, time.perf_counter()-t)
return res
return wrapperdef counter(func):
"""
Décorateur qui affiche le nombre d'appels à une fonction
"""
@wraps(func)
def wrapper(*args, **kwargs):
wrapper.count = wrapper.count + 1
res = func(*args, **kwargs)
print("{} was called {} times".format(func.__name__, wrapper.count))
return res
wrapper.count = 0
return wrapperdef logfunc(func):
"""
Décorateur qui log l'activité d'une fonction.
"""
@wraps(func)
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
s = """
The function *{}* was called with
positional arguments: {}
named arguments: {}
The returned value: {}
"""
print(s.format(func.__name__, args, kwargs, res))
return res
return wrapper@logfunc
@counter
@runtime
def test(num, L):
for i in range(num):
'x' in L
return 'Done'
test(100000, range(10))test 0.12882855499999835
test was called 1 times
The function *test* was called with
positional arguments: (100000, range(0, 10))
named arguments: {}
The returned value: Done
'Done'