Introduction
Dans ce laboratoire (lab), vous allez apprendre ce qu'est un décorateur (decorator) en Python et comment il fonctionne. Les décorateurs sont une fonctionnalité puissante qui vous permet de modifier le comportement d'une fonction sans modifier le code source. Ils sont largement utilisés dans les frameworks et les bibliothèques Python.
Vous allez également apprendre à créer un simple décorateur de journalisation (logging decorator) et à implémenter un décorateur plus complexe pour la validation de fonction. Les fichiers utilisés dans ce laboratoire sont logcall.py, sample.py et validate.py, le fichier validate.py étant modifié.
Créer votre premier décorateur
Qu'est-ce qu'un décorateur ?
En Python, les décorateurs (decorators) sont une syntaxe spéciale qui peut être très utile pour les débutants. Ils vous permettent de modifier le comportement des fonctions ou des méthodes. Imaginez un décorateur comme une fonction qui prend une autre fonction en entrée. Elle retourne ensuite une nouvelle fonction. Cette nouvelle fonction étend souvent ou modifie le comportement de la fonction originale.
Les décorateurs sont appliqués en utilisant le symbole @. Vous placez ce symbole suivi du nom du décorateur directement au-dessus de la définition d'une fonction. C'est un moyen simple de dire à Python que vous voulez utiliser le décorateur sur cette fonction particulière.
Créer un simple décorateur de journalisation
Créons un simple décorateur qui enregistre des informations lorsqu'une fonction est appelée. La journalisation (logging) est une tâche courante dans les applications du monde réel, et utiliser un décorateur pour cela est un excellent moyen de comprendre le fonctionnement des décorateurs.
Tout d'abord, ouvrez l'éditeur VSCode. Dans le répertoire
/home/labex/project, créez un nouveau fichier nommélogcall.py. Ce fichier contiendra notre fonction de décorateur.Ajoutez le code suivant à
logcall.py:
## logcall.py
def logged(func):
print('Adding logging to', func.__name__)
def wrapper(*args, **kwargs):
print('Calling', func.__name__)
return func(*args, **kwargs)
return wrapper
Analysons ce que fait ce code :
- La fonction
loggedest notre décorateur. Elle prend une autre fonction, que nous appelonsfunc, en argument. Cette fonctionfuncest celle à laquelle nous voulons ajouter la journalisation. - Lorsque le décorateur est appliqué à une fonction, il affiche un message. Ce message nous indique que la journalisation est ajoutée à la fonction du nom donné.
- À l'intérieur de la fonction
logged, nous définissons une fonction interne appeléewrapper. Cette fonctionwrapperremplacera la fonction originale.- Lorsque la fonction décorée est appelée, la fonction
wrapperaffiche un message indiquant que la fonction est en cours d'appel. - Elle appelle ensuite la fonction originale (
func) avec tous les arguments qui lui ont été passés. Les*argset**kwargssont utilisés pour accepter n'importe quel nombre d'arguments positionnels et de mots-clés. - Enfin, elle retourne le résultat de la fonction originale.
- Lorsque la fonction décorée est appelée, la fonction
- La fonction
loggedretourne la fonctionwrapper. Cette fonctionwrappersera maintenant utilisée à la place de la fonction originale, ajoutant ainsi la fonctionnalité de journalisation.
Utilisation du décorateur
- Maintenant, dans le même répertoire (
/home/labex/project), créez un autre fichier nommésample.pyavec le code suivant :
## sample.py
from logcall import logged
@logged
def add(x, y):
return x + y
@logged
def sub(x, y):
return x - y
La syntaxe @logged est très importante ici. Elle indique à Python d'appliquer le décorateur logged aux fonctions add et sub. Ainsi, chaque fois que ces fonctions sont appelées, la fonctionnalité de journalisation ajoutée par le décorateur sera exécutée.
Test du décorateur
- Pour tester votre décorateur, ouvrez un terminal dans VSCode. Tout d'abord, changez de répertoire pour le répertoire du projet en utilisant la commande suivante :
cd /home/labex/project
Ensuite, lancez l'interpréteur Python :
python3
- Dans l'interpréteur Python, importez le module
sampleet testez les fonctions décorées :
>>> import sample
Adding logging to add
Adding logging to sub
>>> sample.add(3, 4)
Calling add
7
>>> sample.sub(2, 3)
Calling sub
-1
>>> exit()
Notez que lorsque vous importez le module sample, les messages "Adding logging to..." sont affichés. Cela est dû au fait que le décorateur est appliqué lorsque le module est importé. Chaque fois que vous appelez l'une des fonctions décorées, le message "Calling..." est affiché. Cela montre que le décorateur fonctionne comme prévu.
Ce simple décorateur démontre le concept de base des décorateurs. Il enveloppe la fonction originale avec une fonctionnalité supplémentaire (la journalisation dans ce cas) sans modifier le code de la fonction originale. C'est une fonctionnalité puissante en Python que vous pouvez utiliser dans de nombreux scénarios différents.
Créer un décorateur de validation
Dans cette étape, nous allons créer un décorateur (decorator) plus pratique. En Python, un décorateur est un type spécial de fonction qui peut modifier le comportement d'une autre fonction. Le décorateur que nous allons créer va valider les arguments d'une fonction en fonction des annotations de type (type annotations). Les annotations de type sont un moyen de spécifier les types de données attendus pour les arguments d'une fonction et sa valeur de retour. C'est un cas d'utilisation courant dans les applications du monde réel car cela permet de s'assurer que les fonctions reçoivent les bons types d'entrée, ce qui peut prévenir de nombreux bugs.
Comprendre les classes de validation
Nous avons déjà créé un fichier appelé validate.py pour vous, et il contient quelques classes de validation. Les classes de validation sont utilisées pour vérifier si une valeur répond à certains critères. Pour voir ce qu'il y a dans ce fichier, vous devez l'ouvrir dans l'éditeur VSCode. Vous pouvez le faire en exécutant les commandes suivantes dans le terminal :
cd /home/labex/project
code validate.py
Le fichier contient trois classes :
Validator- C'est une classe de base. Une classe de base fournit un cadre général ou une structure que d'autres classes peuvent hériter. Dans ce cas, elle fournit la structure de base pour la validation.Integer- Cette classe de validateur est utilisée pour s'assurer qu'une valeur est un entier. Si vous passez une valeur non entière à une fonction qui utilise ce validateur, elle lèvera une erreur.PositiveInteger- Cette classe de validateur s'assure qu'une valeur est un entier positif. Donc, si vous passez un entier négatif ou zéro, elle lèvera également une erreur.
Ajouter le décorateur de validation
Maintenant, nous allons ajouter une fonction décorateur nommée validated au fichier validate.py. Ce décorateur effectuera plusieurs tâches importantes :
- Il inspectera les annotations de type d'une fonction. Les annotations de type sont comme de petites notes qui nous indiquent quel type de données la fonction attend.
- Il validera les arguments passés à la fonction par rapport à ces annotations de type. Cela signifie qu'il vérifiera si les valeurs passées à la fonction sont du bon type.
- Il validera également la valeur de retour de la fonction par rapport à son annotation. Ainsi, il s'assurera que la fonction retourne le type de données qu'elle est censée retourner.
- Si la validation échoue, il lèvera des messages d'erreur informatifs. Ces messages vous diront exactement ce qui a mal fonctionné, comme quel argument avait le mauvais type.
Ajoutez le code suivant à la fin du fichier validate.py :
## Add to validate.py
import inspect
import functools
def validated(func):
sig = inspect.signature(func)
print(f'Validating {func.__name__} {sig}')
@functools.wraps(func)
def wrapper(*args, **kwargs):
## Bind arguments to the signature
bound = sig.bind(*args, **kwargs)
errors = []
## Validate each argument
for name, value in bound.arguments.items():
if name in sig.parameters:
param = sig.parameters[name]
if param.annotation != inspect.Parameter.empty:
try:
## Create an instance of the validator and validate the value
if isinstance(param.annotation, type) and issubclass(param.annotation, Validator):
validator = param.annotation()
bound.arguments[name] = validator.validate(value)
except Exception as e:
errors.append(f' {name}: {e}')
## If validation errors, raise an exception
if errors:
raise TypeError('Bad Arguments\n' + '\n'.join(errors))
## Call the function
result = func(*bound.args, **bound.kwargs)
## Validate the return value
if sig.return_annotation != inspect.Signature.empty:
try:
if isinstance(sig.return_annotation, type) and issubclass(sig.return_annotation, Validator):
validator = sig.return_annotation()
result = validator.validate(result)
except Exception as e:
raise TypeError(f'Bad return: {e}') from None
return result
return wrapper
Ce code utilise le module inspect de Python. Le module inspect nous permet d'obtenir des informations sur des objets en temps réel, comme des fonctions. Ici, nous l'utilisons pour examiner la signature de la fonction et valider les arguments en fonction des annotations de type. Nous utilisons également functools.wraps. C'est une fonction auxiliaire qui conserve les métadonnées de la fonction originale, telles que son nom et sa docstring. Les métadonnées sont comme des informations supplémentaires sur la fonction qui nous aident à comprendre ce qu'elle fait.
Tester le décorateur de validation
Créons un fichier pour tester notre décorateur de validation. Nous allons créer un nouveau fichier appelé test_validate.py et y ajouter le code suivant :
## test_validate.py
from validate import Integer, PositiveInteger, validated
@validated
def add(x: Integer, y: Integer) -> Integer:
return x + y
@validated
def pow(x: Integer, y: Integer) -> Integer:
return x ** y
## Test with a class
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
@property
def cost(self):
return self.shares * self.price
@validated
def sell(self, nshares: PositiveInteger):
self.shares -= nshares
Maintenant, nous allons tester notre décorateur dans l'interpréteur Python. Tout d'abord, accédez au répertoire du projet et lancez l'interpréteur Python en exécutant ces commandes dans le terminal :
cd /home/labex/project
python3
Ensuite, dans l'interpréteur Python, nous pouvons exécuter le code suivant pour tester notre décorateur :
>>> from test_validate import add, pow, Stock
Validating add (x: validate.Integer, y: validate.Integer) -> validate.Integer
Validating pow (x: validate.Integer, y: validate.Integer) -> validate.Integer
Validating sell (self, nshares: validate.PositiveInteger) -> <class 'inspect._empty'>
>>>
>>> ## Test with valid inputs
>>> add(2, 3)
5
>>>
>>> ## Test with invalid inputs
>>> add('2', '3')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/labex/project/validate.py", line 75, in wrapper
raise TypeError('Bad Arguments\n' + '\n'.join(errors))
TypeError: Bad Arguments
x: Expected <class 'int'>
y: Expected <class 'int'>
>>>
>>> ## Test valid power
>>> pow(2, 3)
8
>>>
>>> ## Test with negative exponent (produces non - integer result)
>>> pow(2, -1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/labex/project/validate.py", line 83, in wrapper
raise TypeError(f'Bad return: {e}') from None
TypeError: Bad return: Expected <class 'int'>
>>>
>>> ## Test with a class
>>> s = Stock("GOOG", 100, 490.1)
>>> s.sell(50)
>>> s.shares
50
>>>
>>> ## Test with invalid shares
>>> s.sell(-10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/labex/project/validate.py", line 75, in wrapper
raise TypeError('Bad Arguments\n' + '\n'.join(errors))
TypeError: Bad Arguments
nshares: Expected value > 0
>>> exit()
Comme vous pouvez le voir, notre décorateur validated a réussi à appliquer la vérification de type sur les arguments et les valeurs de retour des fonctions. Cela est très utile car cela rend notre code plus robuste. Au lieu de laisser les erreurs de type se propager plus profondément dans le code et causer des bugs difficiles à trouver, nous les capturons aux limites des fonctions.
Résumé
Dans ce laboratoire (lab), vous avez appris ce que sont les décorateurs (decorators) en Python et comment ils fonctionnent. Vous avez également maîtrisé la création d'un simple décorateur de journalisation (logging decorator) pour ajouter un comportement aux fonctions et construit un décorateur plus complexe pour valider les arguments des fonctions en fonction des annotations de type (type annotations). De plus, vous avez appris à utiliser le module inspect pour analyser les signatures des fonctions et functools.wraps pour conserver les métadonnées des fonctions.
Les décorateurs sont une fonctionnalité puissante de Python qui permet d'écrire un code plus maintenable et réutilisable. Ils sont couramment utilisés dans les frameworks et les bibliothèques Python pour des préoccupations transversales telles que la journalisation, le contrôle d'accès et la mise en cache. Vous pouvez maintenant appliquer ces techniques dans vos propres projets Python pour obtenir un code plus propre et plus maintenable.