
# Fichier: python_cheats/cheatsheets/decorators.txt



[OK] 1. DÉCORATEUR SIMPLE

# Un décorateur est une fonction qui prend une fonction en paramètre
# et retourne une nouvelle fonction (wrapper) qui ajoute des fonctionnalités

def my_decorator(func):
    """Décorateur basique qui ajoute du comportement avant et après l'appel"""
    def wrapper():
        print("[BLEU] Avant l'appel de la fonction")
        func()  # Appel de la fonction originale
        print("[BLEU] Après l'appel de la fonction")
    return wrapper  # Retourne la fonction wrapper

@my_decorator  # Syntaxe @ pour appliquer le décorateur
def say_hello():
    print("Hello!")

# say_hello()
# Output:
# [BLEU] Avant l'appel de la fonction
# Hello!
# [BLEU] Après l'appel de la fonction



[OK] 2. DÉCORATEUR AVEC ARGUMENTS (*args, **kwargs)

# Pour gérer les fonctions avec n'importe quels arguments,
# on utilise *args (arguments positionnels) et **kwargs (arguments nommés)

def my_decorator_with_args(func):
    """Décorateur qui peut gérer n'importe quels arguments"""
    def wrapper(*args, **kwargs):
        # *args capture tous les arguments positionnels en tuple
        # **kwargs capture tous les arguments nommés en dictionnaire
        print(f"[ENTREE] Arguments positionnels: {args}")
        print(f"[ENTREE] Arguments nommés: {kwargs}")
        
        result = func(*args, **kwargs)  # On passe tous les arguments à la fonction
        
        print(f"[SORTIE] Résultat: {result}")
        return result  # Important: retourner le résultat
    return wrapper

@my_decorator_with_args
def add(a, b):
    return a + b

@my_decorator_with_args
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

# add(3, 5)
# greet("Alice", greeting="Bonjour")



[OK] 3. DÉCORATEUR AVEC PARAMÈTRES (Factory Pattern)

# Pour créer un décorateur qui accepte des paramètres,
# on utilise 3 niveaux de fonctions imbriquées

def repeat(times):
    """Factory qui crée un décorateur répétant l'exécution"""
    # Niveau 1: reçoit les paramètres du décorateur
    def decorator(func):
        # Niveau 2: reçoit la fonction à décorer
        def wrapper(*args, **kwargs):
            # Niveau 3: exécute la logique du décorateur
            result = None
            for i in range(times):
                print(f"[SYNC] Exécution {i+1}/{times}")
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)  # repeat(3) retourne un décorateur
def greet(name):
    print(f"Hello, {name}!")

# greet("Alice")



[OK] 4. PRÉSERVER LES MÉTADONNÉES (@wraps)

# Sans @wraps, la fonction décorée perd son nom, docstring, etc.
# @wraps copie les métadonnées de la fonction originale

from functools import wraps

def my_decorator_bad(func):
    """Décorateur sans @wraps - perd les métadonnées"""
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

def my_decorator_good(func):
    """Décorateur avec @wraps - préserve les métadonnées"""
    @wraps(func)  # Copie __name__, __doc__, __module__, etc.
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator_bad
def example_bad():
    """Documentation de example_bad"""
    pass

@my_decorator_good
def example_good():
    """Documentation de example_good"""
    pass

# print(example_bad.__name__)  # 'wrapper' [X]
# print(example_good.__name__)  # 'example_good' [OK]



[OK] 5. DÉCORATEUR DE CLASSE

# Un décorateur peut aussi modifier ou enrichir une classe

def add_method(cls):
    """Ajoute une méthode à une classe existante"""
    def new_method(self):
        return f"Nouvelle méthode de {self.__class__.__name__}!"
    
    cls.new_method = new_method  # Ajoute la méthode à la classe
    return cls

def singleton(cls):
    """Pattern Singleton - une seule instance de la classe"""
    instances = {}  # Dictionnaire pour stocker les instances
    
    @wraps(cls)
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    
    return get_instance

@add_method
class MyClass:
    pass

@singleton
class Database:
    def __init__(self):
        print("Connexion à la base de données")

# obj = MyClass()
# print(obj.new_method())
# db1 = Database()  # Crée l'instance
# db2 = Database()  # Retourne la même instance
# print(db1 is db2)  # True



[OK] 6. TIMER DECORATOR (Mesure de performance)

# Mesure le temps d'exécution d'une fonction

import time

def timer(func):
    """Mesure et affiche le temps d'exécution"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()  # Temps de début
        result = func(*args, **kwargs)
        end = time.time()  # Temps de fin
        duration = end - start
        print(f"[TEMPS]  {func.__name__} a pris {duration:.4f} secondes")
        return result
    return wrapper

@timer
def slow_function():
    """Fonction qui prend du temps"""
    time.sleep(1)
    return "Terminé"

# slow_function()



[OK] 7. CACHE DECORATOR (Mémoïsation)

# Stocke les résultats des appels précédents pour éviter les recalculs

from functools import lru_cache

@lru_cache(maxsize=128)  # Cache jusqu'à 128 résultats
def fibonacci(n):
    """Calcul de Fibonacci avec cache pour optimiser"""
    # Sans cache: O(2^n) - exponentiel
    # Avec cache: O(n) - linéaire
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# Cache manuel personnalisé
def memoize(func):
    """Implémentation manuelle d'un cache"""
    cache = {}  # Dictionnaire pour stocker les résultats
    
    @wraps(func)
    def wrapper(*args):
        # Utilise les arguments comme clé du cache
        if args not in cache:
            cache[args] = func(*args)  # Calcule et stocke
        else:
            print(f"[SAUVEGARDE] Résultat en cache pour {args}")
        return cache[args]
    return wrapper

@memoize
def expensive_computation(x, y):
    print(f"[SYNC] Calcul pour {x}, {y}")
    time.sleep(1)
    return x ** y

# expensive_computation(2, 10)  # Calcule
# expensive_computation(2, 10)  # Utilise le cache



[OK] 8. LOGGING DECORATOR

# Enregistre les appels de fonctions pour le débogage

import logging

logging.basicConfig(level=logging.INFO)

def log_calls(func):
    """Log les appels de fonction avec arguments et résultats"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Log avant l'appel
        logging.info(f"[TEL] Appel de {func.__name__}")
        logging.info(f"   Args: {args}, Kwargs: {kwargs}")
        
        try:
            result = func(*args, **kwargs)
            # Log du résultat
            logging.info(f"   [OK] Résultat: {result}")
            return result
        except Exception as e:
            # Log des erreurs
            logging.error(f"   [X] Erreur: {e}")
            raise
    return wrapper

@log_calls
def divide(a, b):
    return a / b

# divide(10, 2)
# divide(10, 0)  # Lève une exception



[OK] 9. AUTHENTIFICATION DECORATOR

# Vérifie les permissions avant d'exécuter une fonction

def requires_auth(func):
    """Vérifie que l'utilisateur est authentifié"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Simulation de vérification d'authentification
        if not is_authenticated():
            raise PermissionError("[VERROUILLE] Authentification requise")
        return func(*args, **kwargs)
    return wrapper

def requires_role(role):
    """Vérifie que l'utilisateur a le bon rôle"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            user_role = get_user_role()
            if user_role != role:
                raise PermissionError(f"[VERROUILLE] Rôle '{role}' requis (vous: '{user_role}')")
            return func(*args, **kwargs)
        return wrapper
    return decorator

# Fonctions helper (simulation)
def is_authenticated():
    return True  # Remplacer par vraie logique

def get_user_role():
    return "admin"

@requires_auth
@requires_role("admin")
def delete_user(user_id):
    return f"Utilisateur {user_id} supprimé"

# delete_user(123)



[OK] 10. VALIDATION DECORATOR

# Valide les arguments avant l'exécution

def validate_args(*validators):
    """Valide chaque argument avec sa fonction de validation"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # Vérifie chaque argument avec son validateur
            for arg, validator in zip(args, validators):
                if not validator(arg):
                    raise ValueError(f"[X] Argument invalide: {arg}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

def validate_types(**type_specs):
    """Valide les types des arguments nommés"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for arg_name, expected_type in type_specs.items():
                if arg_name in kwargs:
                    value = kwargs[arg_name]
                    if not isinstance(value, expected_type):
                        raise TypeError(
                            f"[X] {arg_name} doit être {expected_type.__name__}, "
                            f"pas {type(value).__name__}"
                        )
            return func(*args, **kwargs)
        return wrapper
    return decorator

@validate_args(lambda x: x > 0, lambda y: y > 0)
def divide_positive(a, b):
    """Division de nombres positifs uniquement"""
    return a / b

@validate_types(name=str, age=int, email=str)
def create_user(name, age, email):
    return f"Utilisateur {name}, {age} ans, {email}"

# divide_positive(10, 2)  # [OK]
# divide_positive(-10, 2)  # [X] ValueError
# create_user(name="Alice", age=30, email="alice@example.com")  # [OK]



[OK] 11. RETRY DECORATOR (Gestion des erreurs)

# Réessaye automatiquement en cas d'échec

def retry(max_attempts=3, delay=1, exceptions=(Exception,)):
    """Réessaye la fonction en cas d'échec"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == max_attempts:
                        print(f"[X] Échec après {max_attempts} tentatives")
                        raise
                    print(f"[ATTENTION]  Tentative {attempt}/{max_attempts} échouée: {e}")
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5)
def unstable_api_call():
    """Simule un appel API instable"""
    import random
    if random.random() < 0.7:  # 70% de chance d'échec
        raise ConnectionError("Erreur réseau")
    return "Succès!"

# unstable_api_call()



[OK] 12. RATE LIMITING DECORATOR

# Limite le nombre d'appels par période de temps

def rate_limit(max_calls, period):
    """Limite le nombre d'appels (max_calls par period secondes)"""
    def decorator(func):
        calls = []  # Liste des timestamps des appels
        
        @wraps(func)
        def wrapper(*args, **kwargs):
            now = time.time()
            # Nettoie les appels trop anciens
            calls[:] = [call for call in calls if now - call < period]
            
            if len(calls) >= max_calls:
                raise RuntimeError(
                    f"[INTERDIT] Limite atteinte: {max_calls} appels par {period}s"
                )
            
            calls.append(now)
            return func(*args, **kwargs)
        return wrapper
    return decorator

@rate_limit(max_calls=3, period=10)  # 3 appels par 10 secondes
def api_call():
    print("Appel API réussi")
    return "OK"

# for i in range(5):
#     try:
#         api_call()
#     except RuntimeError as e:
#         print(e)



[OK] 13. DÉCORATEURS MULTIPLES (Ordre d'application)

# Plusieurs décorateurs peuvent être empilés
# Ils s'appliquent de bas en haut (le plus proche de la fonction en premier)

@timer          # 3ème: mesure le temps
@log_calls      # 2ème: log l'appel
@retry(2)       # 1er: ajoute retry
def complex_function(x):
    """Fonction avec plusieurs décorateurs"""
    return x * 2

# Équivalent à:
# complex_function = timer(log_calls(retry(2)(complex_function)))



[OK] 14. PROPERTY DECORATOR (Accesseurs)

# Transforme une méthode en attribut avec getter/setter/deleter

class Person:
    def __init__(self, name, age):
        self._name = name  # Attribut privé (convention _)
        self._age = age
    
    @property  # Getter: accès en lecture
    def name(self):
        """Lecture du nom"""
        return self._name
    
    @name.setter  # Setter: accès en écriture
    def name(self, value):
        """Modification du nom avec validation"""
        if not value or not isinstance(value, str):
            raise ValueError("Le nom doit être une chaîne non vide")
        self._name = value
    
    @name.deleter  # Deleter: suppression
    def name(self):
        """Suppression du nom"""
        print("Suppression du nom")
        del self._name
    
    @property
    def age(self):
        return self._age
    
    @age.setter
    def age(self, value):
        if not isinstance(value, int) or value < 0:
            raise ValueError("L'âge doit être un entier positif")
        self._age = value
    
    # Propriété calculée (lecture seule)
    @property
    def description(self):
        """Propriété calculée - pas de setter"""
        return f"{self.name} a {self.age} ans"

# person = Person("Alice", 30)
# print(person.name)  # Utilise le getter
# person.name = "Bob"  # Utilise le setter
# print(person.description)  # Propriété calculée



[OK] 15. STATICMETHOD ET CLASSMETHOD

# Décorateurs pour méthodes qui ne dépendent pas de l'instance

class MathOperations:
    """Classe avec différents types de méthodes"""
    
    class_variable = "Je suis une variable de classe"
    
    def __init__(self, value):
        self.value = value  # Variable d'instance
    
    # Méthode d'instance normale (reçoit self)
    def instance_method(self):
        """Accède aux données de l'instance"""
        return f"Instance method, value = {self.value}"
    
    @staticmethod  # Ne reçoit ni self ni cls
    def static_method(x, y):
        """Fonction indépendante groupée dans la classe"""
        # Ne peut pas accéder à self ou cls
        # Utilisé pour fonctions utilitaires liées à la classe
        return x + y
    
    @classmethod  # Reçoit cls (la classe) au lieu de self
    def class_method(cls):
        """Accède aux données de la classe (pas de l'instance)"""
        # Peut accéder aux variables de classe
        # Utilisé pour constructeurs alternatifs ou factory methods
        return f"Class method de {cls.__name__}: {cls.class_variable}"
    
    @classmethod
    def from_string(cls, string):
        """Constructeur alternatif à partir d'une chaîne"""
        value = int(string)
        return cls(value)  # Crée une instance

# math_ops = MathOperations(10)
# print(math_ops.instance_method())  # Appel normal
# print(MathOperations.static_method(5, 3))  # Pas besoin d'instance
# print(MathOperations.class_method())  # Accède à la classe
# obj = MathOperations.from_string("42")  # Factory method



[OK] 16. DÉCORATEUR DE CLASSE COMPLET (avec __call__)

# Une classe peut être utilisée comme décorateur en implémentant __call__

class CountCalls:
    """Décorateur de classe qui compte les appels"""
    
    def __init__(self, func):
        """Initialisation du décorateur"""
        self.func = func
        self.count = 0  # Compteur d'appels
        # Copie les métadonnées
        wraps(func)(self)
    
    def __call__(self, *args, **kwargs):
        """Appelé quand on exécute la fonction décorée"""
        self.count += 1
        print(f"[NOMBRE] Appel n°{self.count} de {self.func.__name__}")
        return self.func(*args, **kwargs)
    
    def reset(self):
        """Méthode supplémentaire pour réinitialiser le compteur"""
        self.count = 0

@CountCalls
def say_hello():
    print("Hello!")

# say_hello()  # Appel n°1
# say_hello()  # Appel n°2
# print(f"Total: {say_hello.count}")  # Accède au compteur



[OK] 17. DÉCORATEUR CONTEXTUEL (avec contexte d'exécution)

# Décorateur qui modifie le comportement selon le contexte

def debug_mode(func):
    """Active le mode debug avec détails d'exécution"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        if wrapper.debug:  # Si debug activé
            print(f"[BUG] DEBUG: {func.__name__}")
            print(f"   Args: {args}, Kwargs: {kwargs}")
            print(f"   Module: {func.__module__}")
            import traceback
            print(f"   Call stack:")
            for line in traceback.format_stack()[:-1]:
                print(f"     {line.strip()}")
        
        result = func(*args, **kwargs)
        
        if wrapper.debug:
            print(f"   Résultat: {result}")
        
        return result
    
    wrapper.debug = False  # Flag de debug (peut être modifié)
    return wrapper

@debug_mode
def calculate(x, y):
    return x * y

# calculate(3, 4)  # Mode normal
# calculate.debug = True  # Active le debug
# calculate(3, 4)  # Mode debug



[OK] 18. DÉCORATEUR AVEC ÉTAT PARTAGÉ

# Plusieurs fonctions peuvent partager un état via un décorateur

class SharedState:
    """Décorateur qui partage un état entre fonctions"""
    
    def __init__(self):
        self.data = {}  # État partagé
    
    def __call__(self, func):
        """Permet d'utiliser l'instance comme décorateur"""
        @wraps(func)
        def wrapper(*args, **kwargs):
            # Injecte l'état partagé comme premier argument
            return func(self.data, *args, **kwargs)
        return wrapper

# Création d'une instance de décorateur
shared = SharedState()

@shared
def set_value(state, key, value):
    """Définit une valeur dans l'état partagé"""
    state[key] = value
    return f"Set {key} = {value}"

@shared
def get_value(state, key):
    """Récupère une valeur de l'état partagé"""
    return state.get(key, "Not found")

# set_value("username", "Alice")
# print(get_value("username"))  # "Alice"



[OK] 19. DÉCORATEUR ASYNCHRONE (async/await)

# Pour décorer des fonctions asynchrones (coroutines)

import asyncio

def async_timer(func):
    """Timer pour fonctions asynchrones"""
    @wraps(func)
    async def wrapper(*args, **kwargs):
        start = time.time()
        result = await func(*args, **kwargs)  # await pour fonctions async
        duration = time.time() - start
        print(f"[TEMPS]  {func.__name__} (async) a pris {duration:.4f}s")
        return result
    return wrapper

def async_retry(max_attempts=3):
    """Retry pour fonctions asynchrones"""
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return await func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts:
                        raise
                    print(f"[ATTENTION]  Tentative async {attempt}/{max_attempts} échouée")
                    await asyncio.sleep(1)
        return wrapper
    return decorator

@async_timer
@async_retry(3)
async def fetch_data(url):
    """Simulation d'appel API asynchrone"""
    await asyncio.sleep(1)
    return f"Données de {url}"

# asyncio.run(fetch_data("https://api.example.com"))



[OK] 20. BONNES PRATIQUES ET PATTERNS AVANCÉS


# [OK] Toujours utiliser @wraps pour préserver les métadonnées
# [OK] Utiliser *args, **kwargs pour la flexibilité
# [OK] Retourner le résultat de la fonction originale
# [OK] Gérer les exceptions proprement
# [OK] Documenter le comportement du décorateur

# Pattern: Décorateur paramétrable avec valeurs par défaut
def configurable_decorator(param1=None, param2=None):
    """Décorateur qui peut être utilisé avec ou sans paramètres"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # Utilise param1 et param2
            print(f"Config: {param1}, {param2}")
            return func(*args, **kwargs)
        return wrapper
    
    # Si appelé sans parenthèses (@configurable_decorator)
    if callable(param1):
        func = param1
        param1 = None
        return decorator(func)
    
    # Si appelé avec parenthèses (@configurable_decorator(...))
    return decorator

# Utilisation sans paramètres
@configurable_decorator
def func1():
    pass

# Utilisation avec paramètres
@configurable_decorator(param1="A", param2="B")
def func2():
    pass



[OK] 21. DÉCORATEURS DANS LES FRAMEWORKS POPULAIRES


# Flask (Web)
"""
@app.route('/api/users')
@login_required
@cache.cached(timeout=300)
def get_users():
    return User.query.all()
"""

# Django (Web)
"""
@login_required
@require_http_methods(["GET", "POST"])
@transaction.atomic
def my_view(request):
    pass
"""

# FastAPI (Web moderne)
"""
@app.get("/items/{item_id}")
@cache(expire=60)
async def read_item(item_id: int):
    return {"item_id": item_id}
"""

# Pytest (Tests)
"""
@pytest.fixture
@pytest.mark.parametrize("input,expected", [(1, 2), (2, 3)])
def test_increment(input, expected):
    assert increment(input) == expected
"""



# RÉSUMÉ DES CONCEPTS CLÉS

"""
1. Décorateur = fonction qui modifie une autre fonction
2. Utilise @wraps pour préserver les métadonnées
3. *args/**kwargs pour accepter tous types d'arguments
4. 3 niveaux pour décorateurs paramétrables
5. Classes peuvent être des décorateurs via __call__
6. @property pour accesseurs élégants
7. @staticmethod pour fonctions indépendantes
8. @classmethod pour méthodes de classe
9. Décorateurs async pour coroutines
10. Ordre d'application: de bas en haut
"""