Les décorateurs représentent un mécanisme puissant et flexible en Python. Leur utilité réside dans leur capacité à modifier le comportement de fonctions existantes sans en altérer le code source. Concrètement, un décorateur enveloppe une fonction dans une autre, ajoutant ainsi des fonctionnalités avant et/ou après son exécution. Ils sont un outil précieux pour écrire un code plus propre, réutilisable et modulaire. Cet article vous guidera à travers la compréhension et la création de décorateurs personnalisés.
Prérequis
L’étude des décorateurs en Python exige une certaine familiarité avec des notions de base. Voici une liste des concepts que vous devriez maîtriser avant d’entamer ce tutoriel. Des liens vers des ressources complémentaires sont fournis pour une éventuelle remise à niveau.
Fondamentaux de Python
Les décorateurs sont une notion de niveau intermédiaire/avancé. Par conséquent, il est crucial d’être à l’aise avec les fondations du langage, comme les types de données, les fonctions, les objets et les classes.
Une compréhension de la programmation orientée objet, notamment les notions de getters, setters et constructeurs, est également nécessaire. Si vous débutez en Python, les ressources suivantes pourraient vous être utiles pour démarrer.
Fonctions : Objets de Première Classe
En plus des bases de Python, vous devez aussi comprendre un concept plus avancé : les fonctions sont des objets de première classe. Tout comme les entiers ou les chaînes de caractères, les fonctions sont des objets à part entière. Cela signifie qu’elles peuvent être :
- Passées comme arguments à d’autres fonctions, de la même manière que vous le feriez avec une chaîne ou un entier.
- Retournées par des fonctions, comme n’importe quelle autre valeur.
- Stockées dans des variables.
La principale distinction entre les fonctions et les autres types d’objets est que les fonctions possèdent la méthode magique __call__().
Avec ces connaissances préalables en poche, nous pouvons à présent nous plonger dans le cœur du sujet.
Qu’est-ce qu’un Décorateur en Python ?
Un décorateur Python est, en essence, une fonction qui accepte une autre fonction en paramètre et renvoie une version modifiée de cette dernière. Si une fonction nommée « decorateur » prend comme argument une fonction « fonction_a_decorer » et retourne une fonction « fonction_modifiee », alors « decorateur » est un décorateur.
La fonction « fonction_modifiee » est une version altérée de « fonction_a_decorer », car elle contient un appel à « fonction_a_decorer ». Cependant, « fonction_modifiee » peut exécuter d’autres instructions avant et/ou après cet appel. Pour illustrer ce concept, voici un exemple de code :
# "decorateur" est un décorateur, il prend une autre fonction, "fonction_a_decorer", comme argument. def decorateur(fonction_a_decorer): # Ici, on créé "fonction_modifiee", une version modifiée de "fonction_a_decorer". # "fonction_modifiee" appelera "fonction_a_decorer", mais peut exécuter d'autres instructions avant et après cet appel. def fonction_modifiee(): # Avant d'appeler "fonction_a_decorer", on affiche quelque chose. print("Action effectuée avant l'appel de fonction.") # Ensuite, on exécute "fonction_a_decorer" en l'appelant. fonction_a_decorer() # Puis on affiche quelque chose d'autre après l'exécution de "fonction_a_decorer". print("Action effectuée après l'appel de fonction.") # Enfin, "decorateur" renvoie "fonction_modifiee", une version altérée de "fonction_a_decorer". return fonction_modifiee
Comment Créer un Décorateur en Python ?
Pour démontrer la création et l’utilisation de décorateurs en Python, prenons un exemple concret. Nous allons concevoir un décorateur de journalisation qui enregistrera le nom de la fonction décorée à chaque appel de cette fonction.
Commençons par définir la fonction décorateur. Elle acceptera une fonction, que nous nommerons « func », comme argument. Il s’agit de la fonction que nous souhaitons décorer.
def creer_journaliseur(func): # Le corps de la fonction sera inséré ici.
À l’intérieur de la fonction décorateur, nous allons créer notre fonction modifiée, qui affichera le nom de « func » avant d’exécuter « func ».
# Dans "creer_journaliseur" def fonction_modifiee(): print("Appel de la fonction : ", func.__name__) func()
Ensuite, la fonction « creer_journaliseur » retournera la fonction modifiée. Voici donc la définition complète de notre fonction « creer_journaliseur »:
def creer_journaliseur(func): def fonction_modifiee(): print("Appel de la fonction : ", func.__name__) func() return fonction_modifiee
Notre décorateur est prêt. La fonction « creer_journaliseur » est un exemple simple de fonction décorateur. Elle prend une fonction « func » et renvoie une autre fonction, « fonction_modifiee », qui affiche le nom de « func » avant son exécution.
Comment Utiliser les Décorateurs en Python ?
Pour utiliser un décorateur, on utilise la syntaxe « @ » comme ceci :
@creer_journaliseur def dire_bonjour(): print("Bonjour, tout le monde !")
Nous pouvons maintenant appeler « dire_bonjour() », et le résultat affiché sera :
Appel de la fonction : dire_bonjour "Bonjour, tout le monde !"
Que fait l’annotation « @creer_journaliseur » ? Elle applique tout simplement le décorateur à la fonction « dire_bonjour ». Pour une meilleure compréhension, le code ci-dessous produirait le même résultat que d’utiliser l’annotation « @creer_journaliseur » :
def dire_bonjour(): print("Bonjour, tout le monde !") dire_bonjour = creer_journaliseur(dire_bonjour)
En d’autres termes, l’une des méthodes pour utiliser un décorateur en Python est d’appeler explicitement le décorateur en lui passant la fonction comme argument. L’autre méthode, plus concise, est d’utiliser l’annotation « @ ».
Cette section vous a expliqué comment créer des décorateurs en Python.
Exemples Plus Complexes
L’exemple précédent était assez simple. Il existe des cas plus complexes, par exemple lorsqu’une fonction à décorer prend des arguments, ou lorsque l’on souhaite décorer une classe entière. Nous allons aborder ces deux situations ci-dessous.
Quand la Fonction Prend des Arguments
Lorsqu’une fonction décorée accepte des arguments, la fonction modifiée doit également recevoir ces arguments et les transmettre lors de l’appel final à la fonction non modifiée. Si cela peut paraître confus, clarifions cela avec l’analogie « decorateur », « fonction_a_decorer », et « fonction_modifiee ».
Rappelez-vous que « decorateur » est la fonction de décoration, « fonction_a_decorer » est la fonction que nous décorons et « fonction_modifiee » est la version décorée de « fonction_a_decorer ». Dans ce cas, « fonction_a_decorer » recevra les arguments et les transmettra à « fonction_modifiee » lors de l’appel de cette dernière. Voici un exemple de code :
def decorateur(fonction_a_decorer): def fonction_modifiee(*args, **kwargs): # On peut effectuer une action ici. ___ # Puis on appelle "fonction_a_decorer", en passant les arguments *args et **kwargs. fonction_a_decorer(*args, **kwargs) # On peut aussi effectuer une action ici. ___ return fonction_modifiee
Si les notations *args et **kwargs ne vous sont pas familières, il s’agit simplement de pointeurs vers les arguments positionnels et les arguments nommés respectivement.
Il est important de noter que « fonction_modifiee » a accès aux arguments, et peut donc en faire une validation avant d’appeler « fonction_a_decorer ».
Par exemple, si nous avions un décorateur nommé « Verifier_chaine » qui s’assure que l’argument passé à une fonction est bien une chaîne de caractères, nous pourrions l’implémenter ainsi :
def verifier_chaine(func): def fonction_modifiee(texte): if type(texte) is not str: raise TypeError('L\'argument de la fonction ' + func.__name__ + ' doit être une chaîne de caractères.') else: func(texte) return fonction_modifiee
Nous pourrions décorer la fonction « dire_bonjour » de cette manière :
@verifier_chaine def dire_bonjour(nom): print('Bonjour', nom)
Ensuite, testons le code :
dire_bonjour('Jean') # Devrait s'exécuter correctement dire_bonjour(3) # Devrait lever une exception
La sortie attendue sera :
Bonjour Jean Traceback (most recent call last): File "decorators.py", line 20, in <module> dire_bonjour(3) File "decorators.py", line 7, in fonction_modifiee raise TypeError('L\'argument de la fonction + func._name_ + doit être une chaîne de caractères.') TypeError: L\'argument de la fonction dire_bonjour doit être une chaîne de caractères.
Comme prévu, le script a affiché « Bonjour Jean » car « Jean » est une chaîne. Il a levé une exception en tentant d’afficher « Bonjour 3 », car « 3 » n’est pas une chaîne. Le décorateur « Verifier_chaine » peut être utilisé pour valider les arguments de toute fonction qui exige une chaîne de caractères.
Décorer une Classe
Outre les fonctions, nous pouvons aussi décorer des classes. Dans ce cas, le décorateur va remplacer la méthode constructeur/initialisation de la classe (__init__).
Pour reprendre notre analogie « decorateur », « fonction_a_decorer », et « fonction_modifiee », si « decorateur » est notre décorateur et « fonction_a_decorer » est une classe, alors « decorateur » va décorer la méthode « fonction_a_decorer.__init__ ». Cela sera utile si nous souhaitons exécuter des actions avant l’instanciation d’objets de type « fonction_a_decorer ».
En d’autres termes, le code suivant
def decorateur(func): def nouvelle_fonction(*args, **kwargs): print('Exécution d\'actions avant l\'instanciation.') func(*args, **kwargs) return nouvelle_fonction @decorateur class MaClasse: def __init__(self): print("Dans le constructeur")
est équivalent à
def decorateur(func): def nouvelle_fonction(*args, **kwargs): print('Exécution d\'actions avant l\'instanciation.') func(*args, **kwargs) return nouvelle_fonction class MaClasse: def __init__(self): print("Dans le constructeur") MaClasse.__init__ = decorateur(MaClasse.__init__)
L’instanciation d’un objet de la classe « MaClasse », définie par l’une des deux méthodes, donnera le même résultat :
Exécution d'actions avant l'instanciation. Dans le constructeur
Exemples de Décorateurs en Python
Bien qu’il soit possible de créer ses propres décorateurs, il en existe déjà un certain nombre intégrés à Python. En voici quelques-uns que vous pouvez rencontrer fréquemment :
@staticmethod
Le décorateur « @staticmethod » s’utilise pour indiquer qu’une méthode d’une classe est une méthode statique. Les méthodes statiques peuvent s’exécuter sans qu’il soit nécessaire d’instancier la classe. Dans l’exemple suivant, nous créons une classe « Chien » avec une méthode statique « aboyer ».
class Chien: @staticmethod def aboyer(): print('Wouf, wouf !')
La méthode « aboyer » est accessible de la manière suivante :
Chien.aboyer()
L’exécution de ce code produira :
Wouf, wouf !
Comme mentionné dans la section « Comment Utiliser les Décorateurs », il existe deux façons d’utiliser les décorateurs. La syntaxe « @ » est la plus concise. L’autre méthode consiste à appeler la fonction décorateur, en passant la fonction que nous voulons décorer comme argument. Cela signifie que le code précédent a le même effet que le code ci-dessous :
class Chien: def aboyer(): print('Wouf, wouf !') Chien.aboyer = staticmethod(Chien.aboyer)
Et on peut toujours utiliser la méthode « aboyer » de la même manière :
Chien.aboyer()
Cela produira le même résultat :
Wouf, wouf !
Comme vous pouvez le constater, la première méthode est plus propre et indique clairement que la fonction est statique avant même que vous n’ayez commencé à lire le code. Par conséquent, pour les exemples restants, nous utiliserons la première méthode. N’oubliez pas que la deuxième méthode est une alternative.
@classmethod
Ce décorateur est utilisé pour identifier qu’une méthode est une méthode de classe. À l’instar des méthodes statiques, les méthodes de classe n’exigent pas que la classe soit instanciée pour être appelées.
La principale distinction est que les méthodes de classe ont accès aux attributs de classe, contrairement aux méthodes statiques. Python passe automatiquement la classe comme premier argument à une méthode de classe chaque fois qu’elle est appelée. Pour créer une méthode de classe en Python, on peut utiliser le décorateur « @classmethod ».
class Chien: @classmethod def qui_etes_vous(cls): print("Je suis un " + cls.__name__ + " !")
Pour exécuter ce code, il suffit d’appeler la méthode sans instancier la classe :
Chien.qui_etes_vous()
La sortie sera :
Je suis un Chien !
@property
Le décorateur « @property » est utilisé pour étiqueter une méthode comme un getter de propriété. En reprenant notre exemple de la classe « Chien », nous allons créer une méthode qui récupère le nom du chien.
class Chien: # Création d'une méthode constructeur qui prend le nom du chien comme argument. def __init__(self, nom): # Création d'une propriété privée nommée "nom". # Les doubles tirets bas rendent l'attribut privé. self.__nom = nom @property def nom(self): return self.__nom
Maintenant, nous pouvons accéder au nom du chien comme une propriété normale :
# Création d'une instance de la classe. mon_chien = Chien('Rex') # Accès à la propriété "nom". print("Le nom du chien est :", mon_chien.nom)
L’exécution de ce code affichera :
Le nom du chien est : Rex
@propriété.setter
Le décorateur « @property.setter » sert à créer une méthode setter pour nos propriétés. Pour l’utiliser, vous remplacez « property » par le nom de la propriété pour laquelle vous créez un setter. Par exemple, si vous créez un setter pour la méthode de la propriété « nom », votre décorateur sera « @nom.setter ». Voici un exemple avec la classe « Chien » :
class Chien: # Création d'une méthode constructeur qui prend le nom du chien comme argument. def __init__(self, nom): # Création d'une propriété privée nommée "nom". # Les doubles tirets bas rendent l'attribut privé. self.__nom = nom @property def nom(self): return self.__nom # Création d'un setter pour notre propriété "nom". @nom.setter def nom(self, nouveau_nom): self.__nom = nouveau_nom
Pour tester le setter, nous pouvons utiliser le code suivant :
# Création d'un nouveau chien. mon_chien = Chien('Rex') # Changement du nom du chien. mon_chien.nom="Médor" # Affichage du nouveau nom du chien. print("Le nouveau nom du chien est :", mon_chien.nom)
L’exécution de ce code produira :
Le nouveau nom du chien est : Médor
Importance des Décorateurs en Python
Maintenant que nous avons vu ce que sont les décorateurs et quelques exemples, nous pouvons expliquer pourquoi ils sont importants en Python. Les décorateurs sont cruciaux pour plusieurs raisons, dont voici quelques-unes :
- Ils permettent la réutilisation du code : dans l’exemple du journaliseur ci-dessus, nous pouvons utiliser « @creer_journaliseur » sur n’importe quelle fonction que nous souhaitons. Cela permet d’ajouter une fonction de journalisation à toutes nos fonctions sans l’écrire manuellement pour chacune d’elles.
- Ils favorisent un code modulaire : En reprenant l’exemple de la journalisation, les décorateurs permettent de séparer la fonction principale, dans ce cas « dire_bonjour », des fonctionnalités annexes, ici la journalisation.
- Ils améliorent les frameworks et les librairies : les décorateurs sont largement utilisés dans les frameworks et les librairies Python pour fournir des fonctionnalités complémentaires. Par exemple, dans les frameworks web comme Flask ou Django, les décorateurs sont utilisés pour définir des routes, gérer l’authentification ou appliquer un middleware à des vues spécifiques.
Conclusion
Les décorateurs sont extrêmement utiles ; ils permettent d’étendre les fonctionnalités sans modifier le code initial. Ceci est particulièrement utile pour mesurer les performances de fonctions, enregistrer chaque appel d’une fonction, valider des arguments ou vérifier les autorisations avant l’exécution d’une fonction. Une fois que vous aurez maîtrisé les décorateurs, vous pourrez écrire un code plus propre et plus concis.
Vous pourriez également être intéressé par nos articles sur les tuples et l’utilisation de cURL en Python.