Décorateurs Python

Un décorateur Python offre une manière concise et réutilisable d’étendre une fonction ou une classe. Lisez l’article compagnon Python Decorators: Simple Patterns to Level Up Your Code pour des exemples pratiques et des modèles.

Décorateur de base

Un décorateur dans sa forme la plus simple est une fonction qui prend une autre fonction comme argument et retourne un enveloppeur (wrapper). L’exemple suivant montre la création d’un décorateur et son utilisation.

# Décorateur : une fonction qui prend une autre fonction et retourne un enveloppeur
def your_decorator(func):
  def wrapper():
    # Faire des choses avant func...
    print("Before func!")
    func()  # Appeler la fonction originale
    # Faire des choses après func...
    print("After func!")
  return wrapper  # Retourner la fonction enveloppeuse

# @your_decorator est du sucre syntaxique pour : foo = your_decorator(foo)
@your_decorator
def foo():
  print("Hello World!")

foo()  # Appelle wrapper, qui appelle foo avec un comportement supplémentaire
Before func!
Hello World!
After func!
Quiz

Connectez-vous pour répondre à ce quiz et suivre votre progression d'apprentissage

Qu'est-ce qu'un décorateur en Python ?
A. Une fonction qui prend une autre fonction et retourne une fonction enveloppeuse
B. Un type de classe spécial
C. Un mot-clé intégré de Python
D. Une manière de supprimer des fonctions

Décorateur pour une fonction avec paramètres

# Décorateur qui fonctionne avec des fonctions ayant des paramètres
def your_decorator(func):
  def wrapper(*args,**kwargs):  # Accepter n'importe quels arguments
    # Faire des choses avant func...
    print("Before func!")
    func(*args,**kwargs)  # Passer les arguments à la fonction originale
    # Faire des choses après func...
    print("After func!")
  return wrapper

@your_decorator
def foo(bar):
  print("My name is " + bar)

foo("Jack")  # Les arguments sont passés via l'enveloppeur
Before func!
My name is Jack
After func!

Modèle pour un décorateur de base

Ce modèle est utile pour la plupart des cas d’utilisation de décorateurs. Il est valide pour les fonctions avec ou sans paramètres, et avec ou sans valeur de retour.

import functools

# Modèle de décorateur de meilleure pratique : préserve les métadonnées et la valeur de retour de la fonction
def your_decorator(func):
  @functools.wraps(func)  # Préserve le nom de la fonction, la docstring, etc.
  def wrapper(*args,**kwargs):
    # Faire des choses avant func...
    result = func(*args,**kwargs)  # Appeler la fonction et capturer la valeur de retour
    # Faire des choses après func..
    return result  # Retourner la valeur de retour de la fonction originale
  return wrapper
Quiz

Connectez-vous pour répondre à ce quiz et suivre votre progression d'apprentissage

Que fait @functools.wraps(func) dans un décorateur ?
A. Rend le décorateur plus rapide à exécuter
B. Préserve les métadonnées de la fonction originale (nom, docstring, etc.)
C. Empêche l'appel de la fonction
D. Convertit la fonction en classe

Décorateur avec paramètres

Vous pouvez également définir des paramètres pour que le décorateur les utilise.

import functools

# Fabrique de décorateur : retourne un décorateur basé sur les paramètres
def your_decorator(arg):
  def decorator(func):
    @functools.wraps(func)  # Préserver les métadonnées de la fonction
    def wrapper(*args,**kwargs):
      # Faire des choses avant func potentiellement en utilisant arg...
      result = func(*args,**kwargs)
      # Faire des choses après func potentiellement en utilisant arg...
      return result
    return wrapper
  return decorator  # Retourner la fonction décoratrice réelle

Pour utiliser ce décorateur :

# Utilisation du décorateur avec paramètres : @your_decorator(arg='x') appelle your_decorator('x')
# qui retourne un décorateur qui est ensuite appliqué à foo
@your_decorator(arg = 'x')
def foo(bar):
  return bar

Décorateurs basés sur des classes

Pour décorer une méthode de classe, vous devez définir le décorateur à l’intérieur de la classe. Lorsque seul l’argument implicite self est passé à la méthode, sans aucun argument supplémentaire explicite, vous devez créer un décorateur séparé uniquement pour ces méthodes sans arguments supplémentaires. Un exemple de ceci, montré ci-dessous, est lorsque vous souhaitez intercepter et imprimer les exceptions d’une certaine manière.

# Décorateur de méthode de classe : défini à l'intérieur de la classe
class DecorateMyMethod:

  # Décorateur de méthode statique pour les méthodes avec seulement le paramètre 'self'
  def decorator_for_class_method_with_no_args(method):
    def wrapper_for_class_method(self):  # Ne prend que self
      try:
        return method(self)  # Appeler la méthode originale
      except Exception as e:
        print("\nWARNING: Please make note of the following:\n")
        print(e)
    return wrapper_for_class_method

  def __init__(self,succeed:bool):
    self.succeed = succeed

  @decorator_for_class_method_with_no_args
  def class_action(self):
    if self.succeed:
      print("You succeeded by choice.")
    else:
      raise Exception("Epic fail of your own creation.")

test_succeed = DecorateMyMethod(True)
test_succeed.class_action()
You succeeded by choice.
test_fail = DecorateMyMethod(False)
test_fail.class_action()
Exception: Epic fail of your own creation.

Un décorateur peut également être défini comme une classe au lieu d’une méthode. Ceci est utile pour maintenir et mettre à jour un état, comme dans l’exemple suivant, où nous comptons le nombre d’appels effectués à une méthode :

# Décorateur basé sur une classe : maintient l'état entre les appels
class CountCallNumber:

  def __init__(self, func):
    self.func = func  # Stocker la fonction à décorer
    self.call_number = 0  # Initialiser le compteur d'appels

  def __call__(self, *args, **kwargs):  # Rend l'instance appelable
    self.call_number += 1  # Incrémenter le compteur
    print("This is execution number " + str(self.call_number))
    return self.func(*args, **kwargs)  # Appeler la fonction originale

@CountCallNumber  # Crée une instance de CountCallNumber
def say_hi(name):
  print("Hi! My name is " + name)

say_hi("Jack")  # Appelle CountCallNumber.__call__()
This is execution number 1
Hi! My name is Jack
say_hi("James")
This is execution number 2
Hi! My name is James

Exemple de Compteur

Cet exemple de compteur est inspiré du tutoriel YouTube de Patrick Loeber.

Liens pertinents