Lorsqu’on débute sous Python et qu’on écume différents livres ou cours sur le sujet, on aborde différents concepts facilement assimilables. Très rapidement on arrive à des notions plus complexes faisant la  force du langage et qui nous rendent perplexe au départ. Les décorateurs font partie de ces notions plus évoluées par forcément intuitives mais terriblement efficaces lorsqu’on les maitrise.

Le fonctionnement

En parcourant du code Python, on tombe forcément sur des décorateurs à un moment donné. Typiquement, les gros projets Open-Source en Python usent énormément des décorateurs. Autant en comprendre le principe rapidement pour rendre son code plus souple et efficace. Grossièrement, un décorateur est une méthode qui modifie le comportement d’une autre méthode. Techniquement, un décorateur est :

  1. une méthode qui prend en paramètre une fonction
  2. réalise ou non des opérations.
  3. exécute la méthode en paramètre.
  4. réalise ou non des opérations.
  5. retourne le résultat de la méthode exécutée précédemment.

Lors de l’opération 2 et 4, on peut faire ce que l’on souhaite pour modifier le comportement d’une méthode via l’ajout de fonctionnalités.

Voici comment on déclare et utilise un décorateur :

@mon_decorateur
def ma_methode(*args, **kwargs):

L’exemple classique est de créer un décorateur exécutant un timer afin de calculer le temps d’exécution d’une méthode. Cela signifie qu’on lance le chronomètre lors de l’étape 2 et qu’on l’arrête lors l’étape 4.

Et dans la réalité on l’utilise comment?

Pour illustrer le concept de décorateur, nous allons voir ce qu’il est possible de faire à travers un cas concret d’utilisation.

Tout d’abord, nous avons un module permettant de faire appel à des APIs Rest dans le but de manipuler des ressources. Notre module réalise des POST, GET, PUT, etc. Peu importe de savoir ce que fait réellement ce module car le sujet n’est pas là.  Voici un bout de code permettant de faire des opérations de lectures sur des utilisateurs :

def list_users(self, status_code=None, **kwargs):
    return self._get("/users", headers=self.headers, **kwargs)

def get_user(self, id_user, status_code=None)
    return self._get("/users/%s" % id_user, headers=self.headers)

Pour faire un appel sur la ressource “users” il faut obligatoirement être authentifié au préalable et fournir un token d’authentification dans les headers de la requête. Il nous faut donc une solution pour vérifier que l’authentification a eu lieu et que l’attribut d’instance self.headers soit défini. Plusieurs solutions basiques sont envisageables :

  • faire cette vérification directement dans les méthodes concernées.
  • faire un appel à une autre méthode qui se charge de réaliser cette vérification.

La manière la plus élégante est sans doute de décorer les fonctions qui ont besoin de cette authentification pour fonctionner. C’est bien sûr cette solution qui va être retenue. Voici ce que donne notre décorateur nommé check_initialization.

def check_initialization(func):
    @wraps(func)
    def wrapper(self, *args, **kwargs):
        if not self.url:
            raise Exception("Appel impossible : pas d'authentification")
        return func(self, *args, **kwargs)
    return wrapper

Le décorateur accepte comme argument une méthode (func). On définit une méthode nommée “wrapper” ici qui fait office d’encapsulation. Dans celle-ci, on vérifie qu’il y a eu une authentification et on exécute la méthode décorée. Si ce n’est pas le cas et que donc l’authentification n’a pas eu lieu, une exception est levée et la méthode décorée n’est pas appelée.

Voici comment faire maintenant pour décorer nos deux méthodes listant les utilisateurs :

@check_initialization
def list_users(self, status_code=None, **kwargs):
    return self._get("/users", headers=self.headers, **kwargs)

@check_initialization<
def get_user(self, id_user, status_code=None):
    return self._get("/users/%s" % id_user, headers=self.headers)

On pourrait “customiser” quelque peu notre décorateur lorsque l’exception est levée. Pour rendre plus explicite le message d’erreur, on va afficher le nom de la méthode décorée qui n’a pas pu s’exécuter correctement. On modifie alors le message par celui-ci :

raise Exception("Impossible de lancer '%s' : "
"pas d'authentification" % func.__name__)

Cependant, func._name, ne va pas retourner le nom de la méthode décorée mais “wrapper” qui est le nom de la méthode faisant office d’encapsulation.

Pour ce faire, il faut décorer la méthode “wrapper” par le décorateur “wraps” du module functools. Ce décorateur permet de remplacer les attributs de la méthode qui encapsule (wrapper), par les attributs de la méthode décorée. Le résultat sera alors celui-ci :

from functools import wraps

def check_initialization(func):
    @wraps(func)
    def wrapper(self, *args, **kwargs):
        if not self.url:
            raise Exception("Impossible de lancer '%s' : "
            "pas d'authentification" % func.__name__)
        return func(self, *args, **kwargs)
    return wrapper

Les décorateurs sont de puissants outils en Python pouvant faciliter la vie du développeur. Il ne s’agissait que d’un aperçu sur le sujet, car il est possible d’aller bien plus loin. Par exemple, en décorant une méthode par plusieurs décorateurs, en donnant des arguments à ses décorateurs, en décorant des classes, etc.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Post Navigation