Maitrisez les décorateurs Python - Les décorateurs revisités Partie 1

By jefcolbi on November 07, 2025
Reading time: 7 minutes

Maitrisez les décorateurs Python - Les décorateurs revisités Partie 1

Bonjour le monde,

Nous allons voir ce que sont les décorateurs Python, comment on les ecrit et à quoi ils servent dans la pratique.

Table des matières

I - L'utilisation

C'est bien de découvrir une fonctionnalité ou un concept mais c'est meilleur de savoir à quoi ça peut servir. Découvrons donc 3 cas pratiques où les décorateurs peuvent servir. Ensuite nous verrons les definitions linguistique et programmatique.

1.1 - Création de singleton

Un singleton est une classe qui ne doit être instanciée qu'une seule fois. Pourquoi? Pour diverses raisons, en géneral la classe en question gère l'accès à certaines ressources et on aimerait pas avoir plusieurs gestionnaires dans un même programme. Voici donc un code qui permet de déclarer un singleton.

import functools

existing_instances = {}

def singleton(cls):
    """Transformer une classe en singleton (une seul instance)"""

    @functools.wraps(cls)
    def wrapper_singleton(*args, **kwargs):
        if cls.__name__ not in existing_instances:
            existing_instances[cls.__name__] = cls(*args, **kwargs)
        return existing_instances[cls.__name__]
    return wrapper_singleton

@singleton
class MaClasse:
    pass

Personnellement je n'utilise pas cette methode pour faire un singleton, je prefère redefinir la methode spéciale __new__(). Quoique ce n'est pas évident.

1.2 - Controlez l'accès aux fonctions ou classes

Parfois on voudrait empecher l'accès à certaines fonctions ou classes pendant l'execution du programme dépendamment du contexte. Par exemple sur Django, si on veut empecher un utilisateur d'avoir accès à une page s'il n'est pas connecté on utilise le décorateur @login_required. Justement regardons son code source

def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None):
    """
    Decorator for views that checks that the user is logged in, redirecting
    to the log-in page if necessary.
    """
    actual_decorator = user_passes_test(
        lambda u: u.is_authenticated,
        login_url=login_url,
        redirect_field_name=redirect_field_name
    )
    if function:
        return actual_decorator(function)
    return actual_decorator

Avec ce code aussi simple, on peut limiter l'accès à certaines vues. Cela par opposition à la méthode manuelle que les nouveaux font qui consiste à faire:

def my_view(request):
    if request.user.is_authenticated:
        return redirect(reverse('login'))

    ...

Quoique cette méthode fonctionne, elle manque de flexibilité parce qu'elle exige de refaire la meme chose dans toutes les vues qu'on voudrait proteger.

1.3 - Enregister les classes ou les plugins

Supposons que vous developpez un gros projet et à un niveau vous avez besoin d'enregister et utiliser un ensemble d'entités toutes différentes mais partageant un même point commun comme par exemple la même signature, le même type de retour ou encore la même interface (méthode commune). Mais vous ne connaissez pas la quantité d'entités qui seront crées. Ça peut être une seule, voir 3 voir même 100. A ce stade, vous avez besoin d'un moyen d'enregistrer les entités à l'exécution (runtime registering) et les décorateurs le font à merveille. L'une des choses que les développeurs Flask aiment c'est la simplixité avec laquelle on associe les vues aux endpoints. A la difference de Django où il faut créer une liste d'instances d'Url avec Flask il suffit d'utiliser le décorateur @route présent dans une instance de la classe Flask. Le décorateur en réalité est un méthode de la classe Scaffold https://github.com/pallets/flask/blob/master/src/flask/scaffold.py#L150 Voici le code source:

def route(self, rule, **options):
    
    def decorator(f):
        endpoint = options.pop("endpoint", None)
        self.add_url_rule(rule, endpoint, f, **options)
        return f

    return decorator

J'ai retiré la docstring. Voici un exemple d'utilisation

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello, World!"

Ainsi grace aux décorateurs, la liaison endpoints - vues est devenue simple, rapide et intuitive.

Nous avons fait le tour d'excellents cas d'utilisation des décorateurs dans le monde réel. Maintenant que c'est fait regardons de plus près comme on les implémente.

II - Implémentation

2.1 - Définition

Alors un décorateur c'est même d'abord quoi au juste? On dit qu'un décorateur est une fonction prenant en entrée une fonction et retournant une autre fonction. C'est définition est correcte mais elle est limitée de nos jours. Pour être plus précis, un décorateur est une fonction qui prend entrée un callable et retourne un callable. Qu'est ce qu'on callable? Un callable en Python est un objet qu'on peut appeler comme une fonction c'est à dire faire ceci objet() notez bien les parenthèses. Les fonctions sont évidement des callables, les classes qui implémentent la méthode spéciale __call__() le sont aussi et les lambdas. Puisqu'on peut affecter une fonction (pas le résultat de son appel) à une variable alors meme une variable peut être un callable. Un décorateur est donc censé modifier le comportement de la fonction mais parfois il ne modifie pas le comportement mais peut faire autre chose.

2.2 - Structure d'un décorateur

Un décorateur est normalement constitué de deux fonctions. La première apparaissant comme une fonction normale et la deuxième étant à l'interieur de la première. Comme ceci

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

my_decorator ici correspond actuellement au décorateur lui meme et wrapper correspond à l'enveloppe. C'est le wrapper (enveloppe) qui est en général retourné par le décorateur.

Lorsque une fonction f est décorée, lorsqu'on l'appelle, c'est en réalité la fonction qui est retournée par le décorateur (en general wrapper) qui sera executée.

Si on comprend cela on a déjà compris l'essence des décorateurs.

Pour tester notre décorateur, écrivons ce code:

@my_decorator
def dire_bonjour():
    print("Bonjour!")

## Resultat
>>> dire_bonjour()
Something is happening before the function is called.
Bonjour!
Something is happening after the function is called.

Mais il faut savoir que la notation @decorateur encore appelée "pie" syntax est juste un raccourci pour la réelle syntaxe. Qui est comme ceci

dire_bonjour = my_decorator(dire_bonjour)

## Resultat
>>> dire_bonjour()
Something is happening before the function is called.
Bonjour!
Something is happening after the function is called.

Ca marche parce qu'un décorateur n'est qu'une fonction après tout.

Maintenant, regardons ceci

import functools

def decorator(func):
    @functools.wraps(func)
    def wrapper_decorator(*args, **kwargs):
        # Modifier le comportement avant l'appel
        value = func(*args, **kwargs)
        # Modifier le comportement après l'appel
        return value
    return wrapper_decorator

Voici le template standard, vous pouvez l'utiliser pour écrire votre décorateur. Décorticons les choses: def decorator(func) le décorateur lui meme, le paramètre func est la fonction decorée @functools.wraps(func) la fonction wraps permet de conserver les attributs de la fonction décorée comme par exemple son nom (func.__name__ ) def wrapper_decorator(*args, **kwargs): le wrapper. Comme le wrapper doit accepter les mêmes paramètres que la fonction décorée et comme on ne veut pas tout le temps modifier sa signature, on accepte et recupère simplement tous les paramètres (nommés et positionnels) avec *args et **kwargs value = func(*args, **kwargs) on appelle la fonction décorée on passe les paramètres reçus et on sauvegarde le résultat return value on retourne le resultat de la fonction décorée return wrapper_decorator on retourne le wrapper.

Maintenant voyons un exemple d'application de ce template. En mathématiques il existe certaines fonctions qui ne travaillent que sur les entiers positifs ou parfois on veut limiter l'utilisation d'une fonction à des valeurs entières strictement positives. On serait tenté de mettre le controle du paramètre à l'interieur des fonctions mais non seulement c'est répetitif et c'est moche. En cas d'erreur dans le code du controle on devra le modifier partout. Il est plus simple d'utiliser un décorateur car la fonction et le controle d'accès à la fonction seront ainsi decouplés. Sans plus tarder le code:

import functools
from math import sqrt

def strict_entier_positif(func):
    @functools.wraps(func)
    def wrapper_decorator(n):
        if isinstance(n, int) and n > 0:
            value = func(n)
            return value
        else:
            raise ValueError("{} n'est pas un entier strictement positif".format(n))
    return wrapper_decorator


def racine(n):
    return sqrt(n)

def negative(n): # renvoie toujours un nombre negatif
    return -1 * n

### Tests
>>> racine(-1)
Retraçage (dernier appel le plus récent) : 
  Shell Python, prompt 5, line 1
    # Used internally for debug sandbox under external interpreter
  Shell Python, prompt 4, line 2
    if __name__ == '__main__':
builtins.ValueError: math domain error

>>> racine = strict_entier_positif(racine)
>>> racine(-1)
Retraçage (dernier appel le plus récent) : 
  Shell Python, prompt 7, line 1
    # Used internally for debug sandbox under external interpreter
  Shell Python, prompt 2, line 8
builtins.ValueError: -1 n'est pas un entier strictement positif

>>> negative(-1)
1 # le resultat n'est pas négatif

>>> negative = strict_entier_positif(negative)
>>> negative(-1)
Retraçage (dernier appel le plus récent) : 
  Shell Python, prompt 11, line 1
    # Used internally for debug sandbox under external interpreter
  Shell Python, prompt 2, line 8
builtins.ValueError: -1 n'est pas un entier strictement positif

>>> negative(21)
-21

>>> negative(21.5)
Retraçage (dernier appel le plus récent) : 
  Shell Python, prompt 13, line 1
    # Used internally for debug sandbox under external interpreter
  Shell Python, prompt 2, line 8
builtins.ValueError: 21.5 n'est pas un entier strictement positif

On voit bien comment le décorateur a bloqué l'execution de la fonction décorée quand le paramètre n'était pas un entier strictement supérieur à 0.

Conclusion

Nous avons re-découverts les décorateurs. Allant de ce quoi ils peuvent servir à comment les écrire. Avec le template standard on peut déjà facilement écrire soit même ces super décorateurs. Désormais lorsque vous serez en face d'un décorateur vous saurez un peu ce qu'il fait en interne sans avoir besoin de lire son code source. Mais nous n'avons pas tout fait. Les décorateurs peuvent recevoir aussi des paramètres, ils peuvent même etre chainés, tout ceci nous le verrons dans la 2e partie qui sera consacrée aux décorateurs avancés. Si vous voulez être automatiquement notifiés à la sortie de la partie 2, suivez-moi (en cliquant sur le bouton follow à cote de mon username). Si vous détectez une erreur dans mes codes ou mes textes n'hesitez pas à me laisser un commentaire. Les questions sont aussi les bienvenues.

Comments (0)

-- Signin to comment --

No comments yet. Be the first to comment!