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

Written by @jefcolbi, 2024-04-27, Last update: 2024-04-27

Bonjour le monde,

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

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.

About the blog

Jefcolbi

Ceci est mon blog personnel, où je partage mes connaissances

About the author

Jeff Matt

Je suis développeur Python de metier, entrepreneur à mes heures perdues et lecteur assidu.

Contact: @jefcolbi

From the same author


Cessez d'utiliser partout les listes Python, utilisez plutot deque
Cessez d'utiliser partout les listes Python, utilisez plutot

Utilisation de deque à la place de list

Configurer correctement Django avec Postgresql
Configurer correctement Django avec Postgresql

Bonne configuration de Postgresql pour un projet Django

Ce que je pense de l'unite
Ce que je pense de l'unite

Ce que je pense de l'unité et du vivre ensemble au Cameroun, pays aux 234 tribus.

Introduction au cinéma: Par où commencer
Introduction au cinéma: Par où commencer

Introduction au cinéma. Présentation des films cultes, des sagas classiques et de bons acteurs que tout bon cinéphile se doit de connaitre.

Comment telecharger des fichiers avec python requests
Comment telecharger des fichiers avec python requests

Dans ce tuto, nous allons voir comment télécharger des fichiers avec le package python requests.

SpaceX a installé 29 moteurs Raptor sur une fusée Super Heavy la nuit dernière.
Comment ajouter des articles dans sa boutique Yaknema
Comment ajouter des articles dans sa boutique Yaknema

Vous avez déjà crée votre boutique sur Yaknema et vous souhaitez ajouter des produits/articles ? Cet article est fait pour vous....

Revanced: Comment nioxer Youtube, Spotify etc pour rien
Revanced: Comment nioxer Youtube, Spotify etc pour rien

Un article qui explique comment jouir de youtube premium de manière sécurisée et légale.

Comment entretenir la batterie de son téléphone pendant très très longtemps
Comment entretenir la batterie de son téléphone pendant très

Vous avez tout le temps des problèmes de batterie? Voici le secret pour la conserver pendant plus longtemps.

Comment installer python mysqlclient
Comment installer python mysqlclient

Guide pas à pas sur comment installer le package python mysqlclient sur votre système.

Best way to serve django or flask app using bjoern
Best way to serve django or flask app using bjoern

We will see the best way to serve a django or flask app using bjoern via unix socket.

Les 3 règles fondamentales d'un bon programmeur
Les 3 règles fondamentales d'un bon programmeur

Vous vous êtes toujours demander qu'est ce qui fait la différence entre les bons programmeurs et les moins bons, et comment rejoindre le club des bons...

Comment créer sa boutique sur Yaknema
Comment créer sa boutique sur Yaknema

Vous venez de vous inscrire sur Yaknema et vous ne savez pas comment démarrer? Nous allons vous montrer pas à pas....

Les 8 grands ennemis du cerveau
Les 8 grands ennemis du cerveau

Une liste des ennemis du cerveau à éviter à tout prix

Comment et pourquoi utiliser pre-commit
Comment et pourquoi utiliser pre-commit

Installation, configuration et utilisation de pre-commit pas à pas.

On the same topic



Copyright © 2020 | Powered By | Yaknema SARL