Comment utiliser plusieurs décorateurs sur une seule fonction Python

PythonPythonBeginner
Pratiquer maintenant

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Les décorateurs Python sont un outil puissant qui vous permet de modifier le comportement des fonctions sans changer leur code source. Dans ce tutoriel, nous allons explorer comment utiliser plusieurs décorateurs sur une seule fonction Python, ouvrant ainsi un monde de possibilités pour la réutilisation du code et l'amélioration des fonctions.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/AdvancedTopicsGroup(["Advanced Topics"]) python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/arguments_return("Arguments and Return Values") python/FunctionsGroup -.-> python/lambda_functions("Lambda Functions") python/AdvancedTopicsGroup -.-> python/decorators("Decorators") python/AdvancedTopicsGroup -.-> python/context_managers("Context Managers") subgraph Lab Skills python/function_definition -.-> lab-397700{{"Comment utiliser plusieurs décorateurs sur une seule fonction Python"}} python/arguments_return -.-> lab-397700{{"Comment utiliser plusieurs décorateurs sur une seule fonction Python"}} python/lambda_functions -.-> lab-397700{{"Comment utiliser plusieurs décorateurs sur une seule fonction Python"}} python/decorators -.-> lab-397700{{"Comment utiliser plusieurs décorateurs sur une seule fonction Python"}} python/context_managers -.-> lab-397700{{"Comment utiliser plusieurs décorateurs sur une seule fonction Python"}} end

Comprendre les décorateurs Python

Qu'est-ce que sont les décorateurs en Python?

Les décorateurs en Python sont un moyen puissant et flexible de modifier le comportement d'une fonction ou d'une classe sans changer son code source. Ils permettent de « envelopper » une fonction avec une autre fonction, ce qui permet à la fonction d'enveloppement d'exécuter du code avant et/ou après l'appel de la fonction originale.

Pourquoi utiliser des décorateurs?

Les décorateurs sont utiles pour diverses tâches, telles que :

  • La journalisation (logging) des appels de fonction
  • La mise en cache (caching) des résultats de fonction
  • L'application du contrôle d'accès
  • La mesure du temps d'exécution d'une fonction
  • La répétition des appels de fonction ayant échoué

Comment fonctionnent les décorateurs?

Les décorateurs en Python sont définis à l'aide du symbole @, suivi de la fonction décoratrice. La fonction décoratrice prend une fonction en argument, effectue un traitement supplémentaire, puis retourne une nouvelle fonction qui peut être appelée à la place de la fonction originale.

Voici un exemple simple d'une fonction décoratrice qui journalise les arguments passés à une fonction :

def log_args(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args} and kwargs={kwargs}")
        return func(*args, **kwargs)
    return wrapper

@log_args
def add_numbers(a, b):
    return a + b

result = add_numbers(2, 3)
print(result)

Cela produira la sortie suivante :

Calling add_numbers with args=(2, 3) and kwargs={}
5

Décorateurs imbriqués

Les décorateurs peuvent être imbriqués, ce qui vous permet d'appliquer plusieurs décorateurs à une seule fonction. L'ordre dans lequel les décorateurs sont appliqués est important, car il détermine l'ordre dans lequel les fonctions décoratrices sont exécutées.

def uppercase(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

def reverse(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result[::-1]
    return wrapper

@uppercase
@reverse
def greet(name):
    return f"Hello, {name}!"

print(greet("LabEx"))

Cela produira la sortie suivante :

!XEbal,OLLEH

Arguments de décorateur

Les décorateurs peuvent également prendre des arguments, ce qui vous permet de personnaliser le comportement du décorateur. Cela est utile lorsque vous souhaitez créer un décorateur qui peut être configuré de différentes manières.

def repeat(n):
    def decorator(func):
        def wrapper(*args, **kwargs):
            result = ""
            for _ in range(n):
                result += func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    return f"Hello, {name}!"

print(greet("LabEx"))

Cela produira la sortie suivante :

Hello, LabEx!Hello, LabEx!Hello, LabEx!

Appliquer plusieurs décorateurs

L'ordre d'application des décorateurs

Lorsque vous appliquez plusieurs décorateurs à une fonction, l'ordre dans lequel ils sont appliqués est important. Les décorateurs sont appliqués de bas en haut, ce qui signifie que le décorateur le plus interne est appliqué en premier, et le décorateur le plus externe est appliqué en dernier.

def uppercase(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

def reverse(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result[::-1]
    return wrapper

@uppercase
@reverse
def greet(name):
    return f"Hello, {name}!"

print(greet("LabEx"))

Cela produira la sortie suivante :

!XEBAL,OLLEH

Empiler les décorateurs

Vous pouvez également empiler plusieurs décorateurs sur une seule fonction en les appliquant les uns après les autres. Cela équivaut à imbriquer les décorateurs, mais cela peut rendre le code plus lisible.

def uppercase(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

def reverse(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result[::-1]
    return wrapper

def greet(name):
    return f"Hello, {name}!"

greet_upper_reverse = uppercase(reverse(greet))
print(greet_upper_reverse("LabEx"))

Cela produira la sortie suivante :

!XEBAL,OLLEH

Décorer des méthodes

Les décorateurs peuvent également être utilisés pour modifier le comportement des méthodes dans une classe. Les mêmes principes s'appliquent, mais la fonction décoratrice doit prendre le paramètre self comme premier argument.

def log_method(func):
    def wrapper(self, *args, **kwargs):
        print(f"Calling {func.__name__} on {self.__class__.__name__} with args={args} and kwargs={kwargs}")
        return func(self, *args, **kwargs)
    return wrapper

class Person:
    def __init__(self, name):
        self.name = name

    @log_method
    def greet(self, message):
        return f"{message}, {self.name}!"

person = Person("LabEx")
print(person.greet("Hello"))

Cela produira la sortie suivante :

Calling greet on Person with args=('Hello',) and kwargs={}
Hello, LabEx!

Décorer des classes

Les décorateurs peuvent également être utilisés pour modifier le comportement de classes entières. Dans ce cas, la fonction décoratrice prend une classe en argument et retourne une nouvelle classe avec le comportement souhaité.

def singleton(cls):
    instances = {}

    def wrapper(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return wrapper

@singleton
class MyClass:
    def __init__(self, value):
        self.value = value

obj1 = MyClass(42)
obj2 = MyClass(24)

print(obj1 is obj2)  ## True
print(obj1.value)    ## 42
print(obj2.value)    ## 42

Dans cet exemple, le décorateur singleton garantit qu'une seule instance de la classe MyClass est créée, quelle que soit le nombre de fois où la classe est instanciée.

Exemples de décorateurs dans le monde réel

Décorateur de journalisation (logging)

Un cas d'utilisation courant des décorateurs est la journalisation des appels de fonction. Cela peut être utile pour le débogage, la surveillance ou l'audit.

def log_function_call(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args} and kwargs={kwargs}")
        return func(*args, **kwargs)
    return wrapper

@log_function_call
def add_numbers(a, b):
    return a + b

result = add_numbers(2, 3)
print(result)

Cela produira la sortie suivante :

Calling add_numbers with args=(2, 3) and kwargs={}
5

Décorateur de mise en cache (caching)

Les décorateurs peuvent également être utilisés pour mettre en cache les résultats d'appels de fonction coûteux en termes de performances, améliorant ainsi les performances globales.

from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    if n <= 1:
        return n
    else:
        return (fibonacci(n-1) + fibonacci(n-2))

print(fibonacci(100))

Le décorateur lru_cache du module functools offre un moyen simple d'implémenter une mémoire cache de type Least Recently Used (LRU - Le moins récemment utilisé) pour les résultats de fonction.

Décorateur de contrôle d'accès

Les décorateurs peuvent être utilisés pour appliquer un contrôle d'accès aux fonctions ou méthodes, garantissant que seuls les utilisateurs autorisés peuvent accéder à certaines fonctionnalités.

from functools import wraps

def require_admin(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        if not is_admin(args[0]):
            raise ValueError("Access denied. You must be an admin.")
        return func(*args, **kwargs)
    return wrapper

class User:
    def __init__(self, name, is_admin):
        self.name = name
        self.is_admin = is_admin

    @require_admin
    def delete_user(self, user_to_delete):
        print(f"Deleting user: {user_to_delete.name}")

admin = User("LabEx", True)
regular_user = User("John", False)

admin.delete_user(regular_user)  ## Fonctionne
regular_user.delete_user(admin)  ## Lève une ValueError

Dans cet exemple, le décorateur require_admin vérifie si l'utilisateur appelant la méthode delete_user est un administrateur avant de permettre l'opération de se dérouler.

Décorateur de réessai

Les décorateurs peuvent également être utilisés pour implémenter un mécanisme de réessai pour les fonctions qui peuvent échouer en raison de problèmes temporaires, tels que des erreurs réseau ou des limites de taux d'API.

import time
from functools import wraps

def retry(max_retries=3, delay=1):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            retries = 0
            while retries < max_retries:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"Function {func.__name__} failed. Retrying... ({retries+1}/{max_retries})")
                    retries += 1
                    time.sleep(delay)
            raise Exception(f"Maximum number of retries ({max_retries}) reached for function {func.__name__}")
        return wrapper
    return decorator

@retry(max_retries=3, delay=2)
def flaky_function():
    ## Simule une fonction instable qui échoue 50 % du temps
    if random.random() < 0.5:
        raise Exception("Oops, something went wrong!")
    return "Success!"

print(flaky_function())

Dans cet exemple, le décorateur retry réessaiera automatiquement la fonction flaky_function jusqu'à 3 fois, avec un délai de 2 secondes entre chaque tentative, avant de lever une exception.

Ce ne sont que quelques exemples des nombreux cas d'utilisation réels des décorateurs en Python. Les décorateurs sont un outil puissant et flexible qui peut vous aider à écrire un code plus modulaire, maintenable et réutilisable.

Résumé

À la fin de ce tutoriel, vous aurez une bonne compréhension de la manière d'appliquer plusieurs décorateurs à une seule fonction Python. Vous découvrirez des exemples pratiques et des cas d'utilisation réels, vous permettant d'écrire un code Python plus modulaire, flexible et maintenable.