• Les décorateurs dans Python

      Les décorateurs (wrappers) permet de modifier / étendre le comportement d’une fonction / classe sans en changer le code source.

      Fonctionnement de base

      Un décorateur est une fonction qui prend une fonction en paramètre et retourne une nouvelle fonction.

      def mon_decorateur(fonction_a_decorer):
          def fonction_modifiee():
              print("Avant l'appel de la fonction")
              fonction_a_decorer()
              print("Après l'appel de la fonction")
          return fonction_modifiee
      
      @mon_decorateur
      def ma_fonction():
          print("Fonction originale")
      
      ma_fonction()

      Sortie:

      Avant l'appel de la fonction
      Fonction originale
      Après l'appel de la fonction

      Décorateurs avec arguments

      Les décorateurs peuvent aussi gérer des fonctions avec arguments:

      def decorateur_avec_args(fonction):
          def wrapper(*args, **kwargs):
              print("Arguments reçus:", args, kwargs)
              return fonction(*args, **kwargs)
          return wrapper
      
      @decorateur_avec_args
      def addition(a, b):
          return a + b
      
      print(addition(3, 5))

      Décorateurs avec paramètres

      Vous pouvez aussi créer des décorateurs qui acceptent leurs propres paramètres:

      def repeteur(n):
          def decorateur(fonction):
              def wrapper(*args, **kwargs):
                  for _ in range(n):
                      result = fonction(*args, **kwargs)
                  return result
              return wrapper
          return decorateur
      
      @repeteur(3)
      def dire_bonjour():
          print("Bonjour !")
      
      dire_bonjour()

      Cas d’utilisation courants

      Les décorateurs sont souvent utilisés pour:

      Logging/tracing

      Contrôle d’accès/autorisation

      Temporisation (mesure du temps d’exécution)

      Mise en cache (mémoization)

      Validation des entrées/sorties

      Exemple de décorateur pour mesurer le temps:

      import time
      
      def chronometre(fonction):
          def wrapper(*args, **kwargs):
              debut = time.time()
              result = fonction(*args, **kwargs)
              fin = time.time()
              print(f"Temps d'exécution: {fin - debut:.4f} secondes")
              return result
          return wrapper
      
      @chronometre
      def attendre(secondes):
          time.sleep(secondes)
      
      attendre(1)

      Décorateurs de classe

      Vous pouvez aussi décorer des classes:

      def decorateur_classe(cls):
          class NouvelleClasse(cls):
              def nouvelle_methode(self):
                  print("Méthode ajoutée par le décorateur")
          return NouvelleClasse
      
      @decorateur_classe
      class MaClasse:
          def methode_existante(self):
              print("Méthode existante")
      
      obj = MaClasse()
      obj.methode_existante()
      obj.nouvelle_methode()

      Important: préserver les métadonnées

      L’utilisation de décorateurs peut masquer les métadonnées de la fonction originale. Pour les préserver, utilisez functools.wraps:

      from functools import wraps
      
      def bon_decorateur(f):
          @wraps(f)
          def wrapper(*args, **kwargs):
              """Docstring du wrapper"""
              return f(*args, **kwargs)
          return wrapper
      
      @bon_decorateur
      def exemple():
          """Docstring originale"""
          pass
      
      print(exemple.__name__)  # Affiche 'exemple' au lieu de 'wrapper'
      print(exemple.__doc__)   # Affiche la docstring originale

      Les décorateurs sont un outil extrêmement puissant en Python qui permet d’ajouter des comportements transversaux de manière élégante et réutilisable.

      donne-moi 3 exemples concrets où les décorateurs ont un intéret

      Exemples

      Logging et Debugging

      Tracer l’exécution de certaines fonctions (qui les appelle, avec quels arguments, combien de temps elles prennent, etc.) sans modifier leur code.

      import time
      from functools import wraps
      
      def logger(func):
          @wraps(func)
          def wrapper(*args, **kwargs):
              print(f"Appel de {func.__name__} avec args={args}, kwargs={kwargs}")
              start_time = time.time()
              result = func(*args, **kwargs)
              end_time = time.time()
              print(f"{func.__name__} terminé en {end_time - start_time:.2f}s")
              return result
          return wrapper
      
      @logger
      def calcul_complexe(x, y):
          time.sleep(1)  # Simulation d'un calcul long
          return x * y
      
      print(calcul_complexe(3, 4))

      Sortie :

      Appel de calcul_complexe avec args=(3, 4), kwargs={}
      calcul_complexe terminé en 1.00s
      12

      Intérêt :
      → Réutilisable sur n’importe quelle fonction.
      → Pas de modification du code original.
      → Centralise la logique de logging.

      Contrôle d’accès (Authentification)

      Restreindre l’accès à certaines fonctions uniquement aux utilisateurs autorisés.

      def require_auth(func):
          @wraps(func)
          def wrapper(user, *args, **kwargs):
              if not user.get("is_authenticated", False):
                  raise PermissionError("Accès refusé !")
              return func(user, *args, **kwargs)
          return wrapper
      
      @require_auth
      def acces_restreint(user):
          return f"Bienvenue {user['name']} !"
      
      # Test
      user_valide = {"name": "Alice", "is_authenticated": True}
      user_invalide = {"name": "Bob", "is_authenticated": False}
      
      print(acces_restreint(user_valide))  # OK
      print(acces_restreint(user_invalide))  # Lève une PermissionError

      Intérêt :
      → Évite la duplication de code (pas besoin de vérifier l’authentification dans chaque fonction).
      → Facile à désactiver/en modifiant juste le décorateur.

      Mise en cache (Mémoization)

      Problème : Une fonction coûteuse en calcul est appelée plusieurs fois avec les mêmes arguments (ex: Fibonacci, appel API, etc.).

      from functools import wraps
      
      def memoize(func):
          cache = {}
          @wraps(func)
          def wrapper(*args):
              if args not in cache:
                  cache[args] = func(*args)
              return cache[args]
          return wrapper
      
      @memoize
      def fibonacci(n):
          if n <= 1:
              return n
          return fibonacci(n - 1) + fibonacci(n - 2)
      
      print(fibonacci(50))  # Calcul rapide grâce au cache

      Intérêt :
      → Améliore drastiquement les performances pour les fonctions récursives ou coûteuses.
      → Cache automatique sans modifier la logique de la fonction.

       

       

      Combiner plusieurs décorateurs pour une fonction !

      @logger
      @require_auth
      @memoize
      def fonction_avancee(user, x):
          # ...

       

 

Aucun commentaire

 

Laissez un commentaire