Decorateurs Python

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.

Publications similaires

0 Commentaires
Le plus récent
Le plus ancien Le plus populaire
Commentaires en ligne
Afficher tous les commentaires