Dans cet article, je vais partager avec toi quelques décorateurs Python étonnants qui peuvent réduire ton code de moitié. Cela semble un peu trop irréel ? Eh bien, laisse-moi te montrer tout d’abord comment ils fonctionnent et ensuite pourquoi tu devrais à tout prix les utiliser dans tes projets.
C’est quoi un décorateur Python ?
Les décorateurs Python sont une fonctionnalité puissante qui te permet de modifier le comportement d’une fonction ou d’une classe sans modifier son code source. Il s’agit essentiellement de fonctions qui prennent une autre fonction en argument et renvoient une nouvelle fonction qui enveloppe la fonction d’origine. De cette manière, tu peux ajouter une fonctionnalité ou une logique supplémentaire à la fonction d’origine sans la modifier.
Par exemple, supposons que tu aies une fonction qui imprime un message sur la console :
def hello(): print("Hello world !")
Supposons maintenant que tu souhaites mesurer le temps d’exécution de cette fonction. Tu peux écrire une autre fonction qui utilise le module time
pour calculer le temps d’exécution et qui appelle ensuite la fonction d’origine :
import time def measure_time(func): def wrapper(): start = time.time() func() end = time.time() print(f"Durée d'exécution : {end - start} secondes") return wrapper
Note que la fonction measure_time
renvoie une autre fonction appelée wrapper
, qui est la version modifiée de la fonction d’origine. La fonction wrapper
fait deux choses : elle enregistre l’heure de début et de fin de l’exécution et appelle la fonction d’origine.
Pour utiliser cette fonction, tu pourrais faire quelque chose comme ceci :
hello = measure_time(hello) hello()
Il en résulterait quelque chose comme ceci :
Hello world ! Durée d'exécution : 0.0010998249053955078 secondes
Comme tu peux le constater, nous avons réussi à ajouter une fonctionnalité supplémentaire à la fonction hello
sans modifier son code. Cependant, il existe une manière plus élégante et plus concise de le faire en utilisant les décorateurs. Les décorateurs sont simplement une petite syntaxe qui te permet d’appliquer une fonction à une autre fonction en utilisant le symbole @. Par exemple, nous pourrions réécrire le code précédent comme suit :
@measure_time def hello(): print("Hello world !") hello()
Cela produirait le même résultat que précédemment, mais avec beaucoup moins de code. La ligne @measure_time
équivaut à dire hello = measure_time(hello)
, mais elle est beaucoup plus propre et plus lisible.
Pourquoi utiliser les décorateurs Python ?
Les décorateurs Python sont utiles pour de nombreuses raisons :
- Les décorateurs permettent de réutiliser le code et d’éviter les répétitions. Par exemple, si tu as de nombreuses fonctions qui doivent mesurer leur durée d’exécution, tu peux simplement appliquer le même décorateur à chacune d’entre elles au lieu d’écrire le même code encore et encore.
- Les décorateurs te permettent de séparer les préoccupations et de suivre le principe de la responsabilité unique. Par exemple, si tu as une fonction qui effectue un calcul complexe, tu peux utiliser un décorateur pour gérer le logging, la gestion des erreurs, la mise en cache ou la validation de l’entrée et de la sortie, sans encombrer la logique principale de la fonction.
- Les décorateurs permettent d’étendre les fonctionnalités de fonctions ou de classes existantes sans modifier leur code source. Par exemple, si tu utilises une bibliothèque tierce qui fournit des fonctions ou des classes utiles, mais que tu souhaites leur ajouter des fonctionnalités ou des comportements supplémentaires, tu peux utiliser des décorateurs pour les envelopper et les adapter à tes besoins.
Quelques exemples de décorateurs Python
Il existe de nombreux décorateurs intégrés en Python, tels que @staticmethod
, @classmethod
, @property
, @functools.lru_cache
, @functools.singledispatch
, etc. Tu peux également créer tes propres décorateurs personnalisés à des fins diverses. Voici quelques exemples de décorateurs Python qui peuvent réduire ton code de moitié :
Le décorateur @timer
Ce décorateur est similaire au décorateur @measure_time
que nous avons vu précédemment, mais il peut être appliqué à n’importe quelle fonction qui prend n’importe quel nombre d’arguments et renvoie n’importe quelle valeur. Il utilise également le décorateur functools.wraps
pour préserver le nom et la chaîne de caractères de la fonction d’origine. Voici le code :
import time from functools import wraps def timer(func): @wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(f"Durée d'éxecution : {func.__name__}: {end - start} secondes") return result return wrapper
Tu peux maintenant utiliser ce décorateur pour mesurer la durée d’exécution de n’importe quelle fonction, par exemple :
@timer def factorial(n): """Renvoie la factorielle de n""" if n == 0 or n == 1: return 1 else: return n * factorial(n - 1) @timer def fibonacci(n): """Renvoie le nième nombre de Fibonacci""" if n == 0 or n == 1: return n else: return fibonacci(n - 1) + fibonacci(n - 2) print(factorial(10)) print(fibonacci(10))
Il en résulterait ceci :
Durée d'éxecution : factorial: 9.5367431640625e-07 secondes Durée d'éxecution : fibonacci: 0.0010149478912353516 secondes
Le décorateur @debug
Ce décorateur est utile pour le débogage, car il affiche le nom, les arguments et la valeur de retour de la fonction qu’il enveloppe. Il utilise également le décorateur functools.wraps
pour préserver le nom et la chaîne de caractères de la fonction d’origine. Voici le code :
from functools import wraps def debug(func): @wraps(func) def wrapper(*args, **kwargs): print(f"Appel de {func.__name__} avec les arguments : {args} et les kwargs : {kwargs}") result = func(*args, **kwargs) print(f"{func.__name__} renvoie : {result}") return result return wrapper
Maintenant, tu peux utiliser ce décorateur pour déboguer n’importe quelle fonction, comme par exemple :
@debug def add(x, y): """Renvoie la somme de x et y""" return x + y @debug def greet(name, message="Hello"): """Renvoie un message d'accueil avec le nom""" return f"{message} {name}!" print(add(2, 3)) print(greet("Alice")) print(greet("Bob", message="Salut"))
Il en résulterait ceci :
Appel de add avec les arguments : (2, 3) et les kwargs : {} add renvoie : 5 5 Appel de greet avec les arguments : ('Alice',) et les kwargs : {} greet renvoie : Hello Alice! Hello Alice! Appel de greet avec les arguments : ('Bob',) et les kwargs : {'message': 'Salut'} greet renvoie : Salut Bob! Salut Bob!
Le décorateur @memoize
Ce décorateur est utile pour optimiser les performances des fonctions récursives ou coûteuses, car il met en cache les résultats des appels précédents et les renvoie si les mêmes arguments sont passés à nouveau. Il utilise également le décorateur functools.wraps
pour préserver le nom et la chaîne de caractères de la fonction d’origine. Voici le code :
from functools import wraps def memoize(func): cache = {} @wraps(func) def wrapper(*args): if args in cache: return cache[args] else: result = func(*args) cache[args] = result return result return wrapper
Maintenant, tu peux utiliser ce décorateur pour mémoriser n’importe quelle fonction, comme par exemple :
@memoize def factorial(n): """Renvoie la factorielle de n""" if n == 0 or n == 1: return 1 else: return n * factorial(n - 1) @memoize def fibonacci(n): """Renvoie le nième nombre de Fibonacci""" if n == 0 or n == 1: return n else: return fibonacci(n - 1) + fibonacci(n - 2) print(factorial(10)) print(fibonacci(10))
3628800 55
Le résultat serait le même que précédemment, mais avec un temps d’exécution beaucoup plus rapide, car les résultats sont mis en cache et réutilisés.
Conclusion
Les décorateurs Python sont un moyen puissant et élégant de modifier le comportement des fonctions ou des classes sans changer leur code source. Ils peuvent t’aider à réduire ton code de moitié, à améliorer sa lisibilité, à le réutiliser et à étendre les fonctionnalités du code existant.
J’espère que cet article t’a plu et que tu as appris quelque chose de nouveau.