Patron Décorateur en Python

L’idée centrale du patron de décorateur est l’enveloppement — en créant un objet enveloppeur (décorateur) pour envelopper l’objet d’origine, ce qui permet d’étendre ses fonctionnalités sans modifier l’objet d’origine.

Concepts de base

Le patron de décorateur comprend quatre rôles principaux :

  1. Interface de composant (Component): définit l’interface commune pour l’objet décoré et le décorateur
  2. Composant concret: l’objet d’origine qui doit être décoré
  3. Classe de base du décorateur (Decorator): contient une référence à un objet composant et implémente l’interface du composant
  4. Décorateur concret: implémente la fonctionnalité de décoration spécifique


Syntaxe de base des décorateurs

Décorateurs de fonction

Les décorateurs de fonction sont la forme la plus courante de décorateurs. Ils prennent une fonction en argument et renvoient une nouvelle fonction.

Exemple

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Quelques opérations avant l'exécution de la fonction")
        result = func(*args, **kwargs)
        print("Quelques opérations après l'exécution de la fonction")
        return result
    return wrapper

@my_decorator
def say_hello(name):
    print(f"Bonjour, {name}!")

# Utiliser le décorateur
say_hello("Alice")

Sortie :

函数执行前的一些操作
Hello, Alice!
函数执行后的一些操作

Décorateurs avec paramètres

Si vous devez passer des paramètres au décorateur, vous devez imbriquer une autre couche de fonctions :

Exemple

def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for i in range(times):
                print(f"Exécution {i+1}:")
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def greet(name):
    print(f"Bonjour, {name}!")

greet("Bob")

Décorateurs de classe

Outre les décorateurs de fonctions, Python prend également en charge les décorateurs de classe. Les décorateurs de classe fonctionnent en implémentant__call__des méthodes pour fonctionner.

Décorateur de classe simple

Exemple

class TimerDecorator:
    def __init__(self, func):
        self.func = func
   
    def __call__(self, *args, **kwargs):
        import time
        start_time = time.time()
        result = self.func(*args, **kwargs)
        end_time = time.time()
        print(f"Fonction {self.func.__name__} exécutée en {end_time - start_time:.4f} secondes")
        return result

@TimerDecorator
def calculate_sum(n):
    return sum(range(n))

result = calculate_sum(1000000)
print(f"Résultat du calcul : {result}")

Décorateur de classe avec paramètres

Exemple

class LogDecorator:
    def __init__(self, level="INFO"):
        self.level = level
   
    def __call__(self, func):
        def wrapper(*args, **kwargs):
            print(f"[{self.level}] Appel de la fonction : {func.__name__}")
            print(f"[{self.level}] Arguments : args={args}, kwargs={kwargs}")
            result = func(*args, **kwargs)
            print(f"[{self.level}] Valeur de retour : {result}")
            return result
        return wrapper

@LogDecorator(level="DEBUG")
def multiply(a, b):
    return a * b

multiply(5, 3)

Décorateurs intégrés

Python fournit quelques décorateurs intégrés utiles :

@staticmethod et @classmethod

Exemple

class Calculator:
    @staticmethod
    def add(x, y):
        return x + y
   
    @classmethod
    def multiply(cls, x, y):
        return x * y

# Utiliser une méthode statique
result1 = Calculator.add(5, 3)
print(f"Résultat de la méthode statique : {result1}")

# Utiliser une méthode de classe
result2 = Calculator.multiply(5, 3)
print(f"Résultat de la méthode de classe : {result2}")

@property

Exemple

class Circle:
    def __init__(self, radius):
        self._radius = radius
   
    @property
    def radius(self):
        return self._radius
   
    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("Le rayon doit être un nombre positif")
        self._radius = value
   
    @property
    def area(self):
        return 3.14159 * self._radius ** 2

circle = Circle(5)
print(f"Rayon : {circle.radius}")
print(f"Aire : {circle.area}")

circle.radius = 10
print(f"Nouveau rayon : {circle.radius}")
print(f"Nouvelle aire : {circle.area}")

Scénarios d'application pratiques des décorateurs

1. Journalisation

Exemple

def log_execution(func):
    def wrapper(*args, **kwargs):
        print(f"Début de l'exécution : {func.__name__}")
        try:
            result = func(*args, **kwargs)
            print(f"Terminé avec succès : {func.__name__}")
            return result
        except Exception as e:
            print(f"Échec de l'exécution : {func.__name__}, erreur : {e}")
            raise
    return wrapper

@log_execution
def process_data(data):
    # Simuler le traitement des données
    if not data:
        raise ValueError("Les données ne peuvent pas être vides")
    return [x * 2 for x in data]

# Test
data = [1, 2, 3]
result = process_data(data)
print(f"Résultat du traitement : {result}")

2. Vérification des autorisations

Exemple

def require_login(func):
    def wrapper(user, *args, **kwargs):
        if not user.get('is_authenticated', False):
            raise PermissionError("Utilisateur non connecté")
        return func(user, *args, **kwargs)
    return wrapper

@require_login
def view_profile(user):
    return f"Afficher le profil de l'utilisateur {user['username']}"

# Test
user1 = {'username': 'alice', 'is_authenticated': True}
user2 = {'username': 'bob', 'is_authenticated': False}

print(view_profile(user1))  # Exécution normale
# print(view_profile(user2)) # lèvera PermissionError

3. Décorateur de cache

Exemple

def cache_results(func):
    cache = {}
   
    def wrapper(*args):
        if args in cache:
            print(f"Obtenir le résultat depuis le cache : {args}")
            return cache[args]
        result = func(*args)
        cache[args] = result
        print(f"Calculer et mettre en cache le résultat : {args} -> {result}")
        return result
    return wrapper

@cache_results
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# Tester l'effet du cache
print(fibonacci(5))
print(fibonacci(5))  # Cette fois, il sera récupéré depuis le cache

Ordre d'exécution de plusieurs décorateurs

Lorsque vous utilisez plusieurs décorateurs, leur ordre d'exécution va du bas vers le haut :

Exemple

def decorator1(func):
    def wrapper():
        print("Décorateur 1 - avant")
        func()
        print("Décorateur 1 - après")
    return wrapper

def decorator2(func):
    def wrapper():
        print("Décorateur 2 - avant")
        func()
        print("Décorateur 2 - après")
    return wrapper

@decorator1
@decorator2
def my_function():
    print("Fonction originale")

my_function()

Sortie :

装饰器1 - 前
装饰器2 - 前
原始函数
装饰器2 - 后
装饰器1 - 后

Préservation des métadonnées de fonction

Lorsque vous utilisez des décorateurs, les métadonnées de la fonction d'origine (comme le nom de la fonction, la docstring, etc.) sont écrasées par la fonction d'encapsulation. Vous pouvez utiliserfunctools.wrapspour préserver cette information :

Exemple

from functools import wraps

def preserve_metadata(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """Docstring de la fonction d'encapsulation"""
        return func(*args, **kwargs)
    return wrapper

@preserve_metadata
def example_function():
    """Ceci est la docstring de la fonction d'origine"""
    pass

print(f"Nom de la fonction : {example_function.__name__}")
print(f"Docstring : {example_function.__doc__}")

Exercices pratiques

Exercice 1 : Création d'un décorateur de surveillance des performances

Écrivez un décorateur pour surveiller le temps d'exécution et l'utilisation de la mémoire d'une fonction.

Exemple

import time
import tracemalloc

def performance_monitor(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Démarrer le suivi de la mémoire
        tracemalloc.start()
       
        # Enregistrer l'heure de début
        start_time = time.time()
       
        # Exécuter la fonction
        result = func(*args, **kwargs)
       
        # Enregistrer l'heure de fin
        end_time = time.time()
       
        # Obtenir l'utilisation de la mémoire
        current, peak = tracemalloc.get_traced_memory()
        tracemalloc.stop()
       
        print(f"Rapport de performance pour la fonction {func.__name__}")
        print(f"Temps d'exécution : {end_time - start_time:.4f} secondes")
        print(f"Utilisation mémoire : actuelle {current/1024:.1f} KB, pic {peak/1024:.2f} KB")
       
        return result
    return wrapper

@performance_monitor
def process_large_data():
    """Simuler de grandes quantités de données"""
    data = [i**2 for i in range(100000)]
    return sum(data)

process_large_data()

Exercice 2 : Implémentation d'un décorateur de mécanisme de réessai

Créez un décorateur qui réessaie automatiquement un nombre spécifié de fois lorsqu'une exécution de fonction échoue.

Exemple

def retry(max_attempts=3, delay=1):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            attempts = 0
            while attempts < max_attempts:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    attempts += 1
                    if attempts == max_attempts:
                        print(f"La fonction {func.__name__} a échoué, le nombre maximal de tentatives est atteint")
                        raise
                    print(f"La fonction {func.__name__} a échoué lors de la tentative {attempts} : {e}")
                    print(f"Attente de {delay} secondes avant de réessayer...")
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

@retry(max_attempts=3, delay=2)
def unstable_operation():
    """Simule une opération instable"""
    import random
    if random.random() < 0.7:  # probabilité d'échec de 70%
        raise ValueError("Échec aléatoire")
    return "Opération réussie"

# Test du mécanisme de réessai
result = unstable_operation()
print(f"Résultat final : {result}")

Résumé

Les décorateurs Python sont un outil puissant et flexible qui offre les avantages suivants :

  1. Réutilisation du code: Sépare les préoccupations transversales (comme la journalisation, la mise en cache, la validation) de la logique métier
  2. Extension dynamique: Ajouter de nouvelles fonctionnalités sans modifier le code d'origine
  3. Restez concis: Utilisez@La syntaxe rend le code plus clair et plus lisible
  4. Respecte le principe ouvert-fermé: Ouvert à l'extension, fermé à la modification

Bonnes pratiques d'utilisation des décorateurs :

  • Utilisationfunctools.wrapsPréserver les métadonnées des fonctions
  • Conserver la responsabilité unique du décorateur
  • Gérer correctement les exceptions dans les décorateurs
  • Prêter attention à l'ordre d'exécution des décorateurs
Autres extensions