Python · fonctions · wrappers · architecture du langage

Jeudi 7 mai 2026

Comprendre les décorateurs Python.

Un décorateur permet d'ajouter un comportement autour d'une fonction sans toucher à son code. C'est une idée simple, mais très puissante quand on sait la lire.

Python Décorateurs Jeudi 7 mai 2026 Niveau intermédiaire

Imaginez que vous avez une fonction qui fait quelque chose de précis. Un décorateur, c'est un moyen d'ajouter du comportement autour de cette fonction, avant son exécution, après son exécution, ou même à sa place, sans modifier son code directement.

Concrètement, un décorateur prend une fonction en entrée, l'enveloppe dans une autre fonction, puis retourne quelque chose d'appelable. D'où l'idée de décoration : on habille la fonction sans la réécrire.

À quoi ça sert ?

Les décorateurs sont partout dans le Python réel. Ils servent notamment à :

  • vérifier des arguments avant l'exécution ;
  • modifier ce que la fonction reçoit ou retourne ;
  • mesurer le temps d'exécution ;
  • logger des événements ;
  • mettre en cache des résultats ;
  • gérer des droits d'accès.

La forme de base

Dans sa version la plus simple, un décorateur est juste une fonction qui reçoit une autre fonction et retourne une fonction.

Python
def simple_hello():
    print("Hello from simple function!")


def simple_decorator(function):
    print(f'We are about to call "{function.__name__}"')
    return function


decorated = simple_decorator(simple_hello)
decorated()

Ici, simple_decorator reçoit simple_hello, affiche un message, puis retourne la fonction d'origine. La variable decorated contient donc une fonction appelable.

Le symbole @

En pratique, on n'écrit presque jamais l'appel au décorateur à la main. Python propose la syntaxe @, qui fait exactement la même chose de façon plus lisible.

Python
def simple_decorator(function):
    print(f'We are about to call "{function.__name__}"')
    return function


@simple_decorator
def simple_hello():
    print("Hello from simple function!")


simple_hello()

Le code précédent revient à écrire simple_hello = simple_decorator(simple_hello). C'est du sucre syntaxique, mais du sucre franchement utile.

Créer un wrapper

Le décorateur devient vraiment intéressant quand il retourne une fonction interne. Cette fonction interne, souvent appelée wrapper, peut exécuter du code avant et après la fonction d'origine.

Python
def simple_decorator(own_function):
    def internal_wrapper(*args, **kwargs):
        print(f'"{own_function.__name__}" was called with:')
        print(f"args: {args}")
        print(f"kwargs: {kwargs}")

        result = own_function(*args, **kwargs)

        print("Decorator is still operating")
        return result

    return internal_wrapper


@simple_decorator
def combiner(*args, **kwargs):
    print("Hello from the decorated function")
    print("Received:", args, kwargs)


combiner("a", "b", exec="yes")

Le couple *args et **kwargs rend le décorateur générique : peu importe la signature de la fonction décorée, les arguments sont transmis correctement.

Exemple concret : mesurer le temps d'exécution

Un cas d'usage très courant consiste à mesurer combien de temps prend une fonction. Plutôt que de dupliquer ce code partout, on le place dans un décorateur.

Python
import time


def timing_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        execution_time = time.time() - start_time

        print(
            f"La fonction {func.__name__} "
            f"a pris {execution_time:.2f} secondes."
        )
        return result

    return wrapper


@timing_decorator
def add(a, b):
    time.sleep(1)
    return a + b


print(add(5, 3))

La fonction add ne sait rien du timing. Elle additionne. Le décorateur, lui, mesure. Chacun son travail.

Gérer les droits d'accès

Les frameworks web utilisent souvent des décorateurs pour protéger une route ou une action. Voici une version minimaliste du principe.

Python
class User:
    def __init__(self, username, authenticated=False):
        self.username = username
        self.authenticated = authenticated


def authentication_required(func):
    def wrapper(user, *args, **kwargs):
        if not user.authenticated:
            raise PermissionError("Accès refusé.")

        return func(user, *args, **kwargs)

    return wrapper


@authentication_required
def sensitive_operation(user):
    return f"Opération sensible pour {user.username}."


guest = User(username="invite")
admin = User(username="admin", authenticated=True)

try:
    print(sensitive_operation(guest))
except PermissionError as error:
    print(error)

print(sensitive_operation(admin))

Décorateurs avec paramètres

Parfois, le décorateur lui-même a besoin de recevoir une configuration. Dans ce cas, on ajoute une couche supplémentaire : une fonction qui reçoit les paramètres et retourne le vrai décorateur.

Python
def authorization_required(required_roles):
    def decorator(func):
        def wrapper(user, *args, **kwargs):
            has_role = any(role in user.roles for role in required_roles)

            if not has_role:
                raise PermissionError(
                    f"{user.username} n'a pas les droits requis."
                )

            return func(user, *args, **kwargs)

        return wrapper

    return decorator


class User:
    def __init__(self, username, roles=None):
        self.username = username
        self.roles = roles or []


@authorization_required(["admin", "moderator"])
def admin_operation(user):
    return f"Opération admin pour {user.username}."


regular_user = User("rachid", roles=["user"])
admin_user = User("admin", roles=["admin"])

try:
    print(admin_operation(regular_user))
except PermissionError as error:
    print(error)

print(admin_operation(admin_user))

Quand Python lit @authorization_required(["admin"]), il appelle d'abord authorization_required, récupère un décorateur, puis applique ce décorateur à la fonction.

Empiler plusieurs décorateurs

On peut appliquer plusieurs décorateurs sur une même fonction. C'est très pratique quand on veut vérifier plusieurs règles avant d'autoriser une opération : par exemple, d'abord vérifier que l'utilisateur est connecté, puis vérifier qu'il possède le bon rôle.

Python
class User:
    def __init__(self, username, authenticated=False, roles=None):
        self.username = username
        self.authenticated = authenticated
        self.roles = roles or []


def login_required(func):
    def wrapper(user, *args, **kwargs):
        if not user.authenticated:
            raise PermissionError("Vous devez être connecté.")

        return func(user, *args, **kwargs)

    return wrapper


def permission_required(required_role):
    def decorator(func):
        def wrapper(user, *args, **kwargs):
            if required_role not in user.roles:
                raise PermissionError(
                    f"{user.username} n'a pas le rôle {required_role}."
                )

            return func(user, *args, **kwargs)

        return wrapper

    return decorator


@login_required
@permission_required("admin")
def delete_user(user, target_username):
    return f"{user.username} supprime le compte {target_username}."


guest = User("invite")
member = User("rachid", authenticated=True, roles=["user"])
admin = User("admin", authenticated=True, roles=["admin"])


for current_user in [guest, member, admin]:
    try:
        print(delete_user(current_user, "ancien_compte"))
    except PermissionError as error:
        print(error)

Sans le @, cette déclaration reviendrait à écrire delete_user = login_required(permission_required("admin")(delete_user)). À l'exécution, login_required vérifie donc d'abord la connexion. Si l'utilisateur est connecté, permission_required vérifie ensuite le rôle.

Décorateurs sous forme de classe

Un décorateur n'est pas forcément une fonction. Une classe peut aussi décorer une fonction si elle implémente __call__. C'est utile quand le décorateur doit conserver un état interne.

Python
class CountCalls:
    def __init__(self, func):
        self.func = func
        self.calls = 0

    def __call__(self, *args, **kwargs):
        self.calls += 1
        print(f"{self.func.__name__} appelée {self.calls} fois")
        return self.func(*args, **kwargs)


@CountCalls
def greet(name):
    return f"Bonjour {name}"


print(greet("Rachid"))
print(greet("Python"))

Ici, l'objet décorateur conserve le nombre d'appels dans self.calls. C'est plus naturel avec une classe qu'avec une fonction imbriquée.

À retenir

  • Un décorateur reçoit une fonction et retourne une fonction ou un objet appelable.
  • La syntaxe @decorator équivaut à réassigner la fonction décorée.
  • Le wrapper permet d'ajouter du comportement avant, après ou autour de l'appel.
  • *args et **kwargs rendent le décorateur compatible avec plusieurs signatures.
  • Une couche supplémentaire permet de créer des décorateurs avec paramètres.
  • Une classe avec __call__ peut aussi servir de décorateur.