Introduction
Dans ce laboratoire (lab), vous allez apprendre à utiliser les décorateurs (decorators) en Python, une fonctionnalité puissante qui peut modifier le comportement des fonctions et des méthodes. Les décorateurs sont couramment utilisés pour des tâches telles que la journalisation (logging), la mesure des performances, le contrôle d'accès et la vérification des types.
Vous allez apprendre à enchaîner plusieurs décorateurs, à créer des décorateurs qui acceptent des paramètres, à conserver les métadonnées des fonctions lors de l'utilisation de décorateurs et à appliquer des décorateurs à différents types de méthodes de classe. Les fichiers avec lesquels vous allez travailler sont logcall.py, validate.py et sample.py.
Conservation des métadonnées de fonction dans les décorateurs
En Python, les décorateurs (decorators) sont un outil puissant qui vous permet de modifier le comportement des fonctions. Cependant, lorsque vous utilisez un décorateur pour envelopper une fonction, il y a un petit problème. Par défaut, les métadonnées de la fonction d'origine, telles que son nom, sa chaîne de documentation (docstring) et ses annotations, sont perdues. Les métadonnées sont importantes car elles facilitent l'introspection (l'examen de la structure du code) et la génération de documentation. Vérifions d'abord ce problème.
Ouvrez votre terminal dans le WebIDE. Nous allons exécuter quelques commandes Python pour voir ce qui se passe lorsque nous utilisons un décorateur. Les commandes suivantes créeront une simple fonction add enveloppée dans un décorateur, puis afficheront la fonction et sa docstring.
cd ~/project
python3 -c "from logcall import logged; @logged
def add(x,y):
'Adds two things'
return x+y
print(add)
print(add.__doc__)"
Lorsque vous exécutez ces commandes, vous verrez une sortie similaire à ceci :
<function wrapper at 0x...>
None
Notez que au lieu d'afficher le nom de la fonction comme add, il affiche wrapper. Et la docstring, qui devrait être 'Adds two things', est None. Cela peut être un gros problème lorsque vous utilisez des outils qui dépendent de ces métadonnées, comme les outils d'introspection ou les générateurs de documentation.
Résolution du problème avec functools.wraps
Le module functools de Python vient à la rescousse. Il fournit un décorateur wraps qui peut nous aider à conserver les métadonnées de la fonction. Voyons comment nous pouvons modifier notre décorateur logged pour utiliser wraps.
- Tout d'abord, ouvrez le fichier
logcall.pydans le WebIDE. Vous pouvez accéder au répertoire du projet en utilisant la commande suivante dans le terminal :
cd ~/project
- Maintenant, mettez à jour le décorateur
loggeddanslogcall.pyavec le code suivant. Le décorateur@wraps(func)est la clé ici. Il copie toutes les métadonnées de la fonction d'originefuncvers la fonction enveloppante.
from functools import wraps
def logged(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
Le décorateur
@wraps(func)a un rôle important. Il prend toutes les métadonnées (comme le nom, la docstring et les annotations) de la fonction d'originefuncet les attache à la fonctionwrapper. De cette façon, lorsque nous utilisons la fonction décorée, elle aura les bonnes métadonnées.Testons notre décorateur amélioré. Exécutez les commandes suivantes dans le terminal :
python3 -c "from logcall import logged; @logged
def add(x,y):
'Adds two things'
return x+y
print(add)
print(add.__doc__)"
Maintenant, vous devriez voir :
<function add at 0x...>
Adds two things
Génial ! Le nom de la fonction et la docstring sont conservés. Cela signifie que notre décorateur fonctionne maintenant comme prévu et que les métadonnées de la fonction d'origine sont intactes.
Correction du décorateur validate.py
Maintenant, appliquons la même correction au décorateur validated dans validate.py. Ce décorateur est utilisé pour valider les types des arguments de fonction et la valeur de retour en fonction des annotations de la fonction.
Ouvrez
validate.pydans le WebIDE.Mettez à jour le décorateur
validatedavec le décorateur@wraps. Le code suivant montre comment le faire. Le décorateur@wraps(func)est ajouté à la fonctionwrapperà l'intérieur du décorateurvalidatedpour conserver les métadonnées.
from functools import wraps
class Integer:
@classmethod
def __instancecheck__(cls, x):
return isinstance(x, int)
def validated(func):
@wraps(func)
def wrapper(*args, **kwargs):
## Get function annotations
annotations = func.__annotations__
## Check arguments against annotations
for arg_name, arg_value in zip(func.__code__.co_varnames, args):
if arg_name in annotations and not isinstance(arg_value, annotations[arg_name]):
raise TypeError(f'Expected {arg_name} to be {annotations[arg_name].__name__}')
## Run the function and get the result
result = func(*args, **kwargs)
## Check the return value
if 'return' in annotations and not isinstance(result, annotations['return']):
raise TypeError(f'Expected return value to be {annotations["return"].__name__}')
return result
return wrapper
- Testons que notre décorateur
validatedconserve maintenant les métadonnées. Exécutez les commandes suivantes dans le terminal :
python3 -c "from validate import validated, Integer; @validated
def multiply(x: Integer, y: Integer) -> Integer:
'Multiplies two integers'
return x * y
print(multiply)
print(multiply.__doc__)"
Vous devriez voir :
<function multiply at 0......>
Multiplies two integers
Maintenant, les deux décorateurs, logged et validated, conservent correctement les métadonnées des fonctions qu'ils décorent. Cela garantit que lorsque vous utilisez ces décorateurs, les fonctions auront toujours leurs noms, docstrings et annotations d'origine, ce qui est très utile pour la lisibilité et la maintenance du code.
Création de décorateurs avec des arguments
Jusqu'à présent, nous avons utilisé le décorateur @logged, qui affiche toujours un message fixe. Mais que faire si vous souhaitez personnaliser le format du message ? Dans cette section, nous allons apprendre à créer un nouveau décorateur qui peut accepter des arguments, vous offrant ainsi plus de flexibilité dans l'utilisation des décorateurs.
Compréhension des décorateurs paramétrés
Un décorateur paramétré est un type spécial de fonction. Au lieu de modifier directement une autre fonction, il retourne un décorateur. La structure générale d'un décorateur paramétré ressemble à ceci :
def decorator_with_args(arg1, arg2, ...):
def actual_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
## Use arg1, arg2, ... here
## Call the original function
return func(*args, **kwargs)
return wrapper
return actual_decorator
Lorsque vous utilisez @decorator_with_args(value1, value2) dans votre code, Python appelle d'abord decorator_with_args(value1, value2). Cet appel retourne le véritable décorateur, qui est ensuite appliqué à la fonction qui suit la syntaxe @. Ce processus en deux étapes est essentiel au fonctionnement des décorateurs paramétrés.
Création du décorateur logformat
Créons un décorateur @logformat(fmt) qui prend une chaîne de formatage comme argument. Cela nous permettra de personnaliser le message de journalisation.
- Ouvrez
logcall.pydans le WebIDE et ajoutez le nouveau décorateur. Le code ci-dessous montre comment définir à la fois le décorateurloggedexistant et le nouveau décorateurlogformat:
from functools import wraps
def logged(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
def logformat(fmt):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(fmt.format(func=func))
return func(*args, **kwargs)
return wrapper
return decorator
Dans le décorateur logformat, la fonction externe logformat prend une chaîne de formatage fmt comme argument. Elle retourne ensuite la fonction decorator, qui est le véritable décorateur qui modifie la fonction cible.
- Maintenant, testons notre nouveau décorateur en modifiant
sample.py. Le code suivant montre comment utiliser à la fois les décorateursloggedetlogformatsur différentes fonctions :
from logcall import logged, logformat
@logged
def add(x, y):
"Adds two numbers"
return x + y
@logged
def sub(x, y):
"Subtracts y from x"
return x - y
@logformat('{func.__code__.co_filename}:{func.__name__}')
def mul(x, y):
"Multiplies two numbers"
return x * y
Ici, les fonctions add et sub utilisent le décorateur logged, tandis que la fonction mul utilise le décorateur logformat avec une chaîne de formatage personnalisée.
- Exécutez le fichier
sample.pymis à jour pour voir les résultats. Ouvrez votre terminal et exécutez la commande suivante :
cd ~/project
python3 -c "import sample; print(sample.add(2, 3)); print(sample.mul(2, 3))"
Vous devriez voir une sortie similaire à :
Calling add
5
sample.py:mul
6
Cette sortie montre que le décorateur logged affiche le nom de la fonction comme prévu, et le décorateur logformat utilise la chaîne de formatage personnalisée pour afficher le nom du fichier et le nom de la fonction.
Redéfinition du décorateur logged en utilisant logformat
Maintenant que nous avons un décorateur logformat plus flexible, nous pouvons redéfinir notre décorateur logged d'origine en l'utilisant. Cela nous aidera à réutiliser le code et à maintenir un format de journalisation cohérent.
- Mettez à jour
logcall.pyavec le code suivant :
from functools import wraps
def logformat(fmt):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(fmt.format(func=func))
return func(*args, **kwargs)
return wrapper
return decorator
## Define logged using logformat
logged = lambda func: logformat("Calling {func.__name__}")(func)
Ici, nous utilisons une fonction lambda pour définir le décorateur logged en termes du décorateur logformat. La fonction lambda prend une fonction func et applique le décorateur logformat avec une chaîne de formatage spécifique.
- Testez que le décorateur
loggedredéfini fonctionne toujours. Ouvrez votre terminal et exécutez la commande suivante :
cd ~/project
python3 -c "from logcall import logged; @logged
def greet(name):
return f'Hello, {name}'
print(greet('World'))"
Vous devriez voir :
Calling greet
Hello, World
Cela montre que le décorateur logged redéfini fonctionne comme prévu, et nous avons réussi à réutiliser le décorateur logformat pour obtenir un format de journalisation cohérent.
Application de décorateurs aux méthodes de classe
Maintenant, nous allons explorer comment les décorateurs interagissent avec les méthodes de classe. Cela peut être un peu délicat car Python a différents types de méthodes : les méthodes d'instance, les méthodes de classe, les méthodes statiques et les propriétés. Les décorateurs sont des fonctions qui prennent une autre fonction et étendent le comportement de cette dernière sans la modifier explicitement. Lorsque nous appliquons des décorateurs aux méthodes de classe, nous devons faire attention à la façon dont ils fonctionnent avec ces différents types de méthodes.
Compréhension du défi
Voyons ce qui se passe lorsque nous appliquons notre décorateur @logged à différents types de méthodes. Le décorateur @logged est probablement utilisé pour enregistrer des informations sur les appels de méthode.
- Créez un nouveau fichier
methods.pydans le WebIDE. Ce fichier contiendra notre classe avec différents types de méthodes décorées avec le décorateur@logged.
from logcall import logged
class Spam:
@logged
def instance_method(self):
print("Instance method called")
return "instance result"
@logged
@classmethod
def class_method(cls):
print("Class method called")
return "class result"
@logged
@staticmethod
def static_method():
print("Static method called")
return "static result"
@logged
@property
def property_method(self):
print("Property method called")
return "property result"
Dans ce code, nous avons une classe Spam avec quatre types différents de méthodes. Chaque méthode est décorée avec le décorateur @logged, et certaines sont également décorées avec d'autres décorateurs intégrés comme @classmethod, @staticmethod et @property.
- Testons comment cela fonctionne. Nous allons exécuter une commande Python dans le terminal pour appeler ces méthodes et voir la sortie.
cd ~/project
python3 -c "from methods import Spam; s = Spam(); print(s.instance_method()); print(Spam.class_method()); print(Spam.static_method()); print(s.property_method)"
Lorsque vous exécutez cette commande, vous pourriez remarquer quelques problèmes :
- Le décorateur
@propertypeut ne pas fonctionner correctement avec notre décorateur@logged. Le décorateur@propertyest utilisé pour définir une méthode comme une propriété, et il a un mode de fonctionnement spécifique. Lorsqu'il est combiné avec le décorateur@logged, il pourrait y avoir des conflits. - L'ordre des décorateurs est important pour
@classmethodet@staticmethod. L'ordre dans lequel les décorateurs sont appliqués peut changer le comportement de la méthode.
L'ordre des décorateurs
Lorsque vous appliquez plusieurs décorateurs, ils sont appliqués de bas en haut. Cela signifie que le décorateur le plus proche de la définition de la méthode est appliqué en premier, puis ceux au - dessus sont appliqués séquentiellement. Par exemple :
@decorator1
@decorator2
def func():
pass
Cela équivaut à :
func = decorator1(decorator2(func))
Dans cet exemple, decorator2 est appliqué à func en premier, puis decorator1 est appliqué au résultat de decorator2(func).
Correction de l'ordre des décorateurs
Mettons à jour notre fichier methods.py pour corriger l'ordre des décorateurs. En changeant l'ordre des décorateurs, nous pouvons nous assurer que chaque méthode fonctionne comme prévu.
from logcall import logged
class Spam:
@logged
def instance_method(self):
print("Instance method called")
return "instance result"
@classmethod
@logged
def class_method(cls):
print("Class method called")
return "class result"
@staticmethod
@logged
def static_method():
print("Static method called")
return "static result"
@property
@logged
def property_method(self):
print("Property method called")
return "property result"
Dans cette version mise à jour :
- Pour
instance_method, l'ordre n'a pas d'importance. Les méthodes d'instance sont appelées sur une instance de la classe, et le décorateur@loggedpeut être appliqué dans n'importe quel ordre sans affecter sa fonctionnalité de base. - Pour
class_method, nous appliquons@classmethodaprès@logged. Le décorateur@classmethodchange la façon dont la méthode est appelée, et en l'appliquant après@logged, nous nous assurons que la journalisation fonctionne correctement. - Pour
static_method, nous appliquons@staticmethodaprès@logged. De même que pour@classmethod, le décorateur@staticmethoda son propre comportement, et l'ordre avec le décorateur@loggeddoit être correct. - Pour
property_method, nous appliquons@propertyaprès@logged. Cela garantit que le comportement de la propriété est maintenu tout en obtenant la fonctionnalité de journalisation.
- Testons le code mis à jour. Nous allons exécuter la même commande que précédemment pour voir si les problèmes sont résolus.
cd ~/project
python3 -c "from methods import Spam; s = Spam(); print(s.instance_method()); print(Spam.class_method()); print(Spam.static_method()); print(s.property_method)"
Vous devriez maintenant voir une journalisation correcte pour tous les types de méthodes :
Calling instance_method
Instance method called
instance result
Calling class_method
Class method called
class result
Calling static_method
Static method called
static result
Calling property_method
Property method called
property result
Bonnes pratiques pour les décorateurs de méthode
Lorsque vous travaillez avec des décorateurs de méthode, suivez ces bonnes pratiques :
- Appliquez les décorateurs de transformation de méthode (
@classmethod,@staticmethod,@property) après vos décorateurs personnalisés. Cela garantit que les décorateurs personnalisés peuvent effectuer leur journalisation ou d'autres opérations en premier, puis les décorateurs intégrés peuvent transformer la méthode comme prévu. - Sachez que l'exécution du décorateur se produit au moment de la définition de la classe, pas au moment de l'appel de la méthode. Cela signifie que tout code de configuration ou d'initialisation dans le décorateur sera exécuté lorsque la classe est définie, pas lorsque la méthode est appelée.
- Pour les cas plus complexes, vous devrez peut - être créer des décorateurs spécialisés pour différents types de méthodes. Les différents types de méthodes ont des comportements différents, et un décorateur universel peut ne pas fonctionner dans toutes les situations.
Création d'un décorateur de vérification de type avec des arguments
Dans les étapes précédentes, nous avons appris à connaître le décorateur @validated. Ce décorateur est utilisé pour appliquer les annotations de type dans les fonctions Python. Les annotations de type sont un moyen de spécifier les types attendus pour les arguments et les valeurs de retour des fonctions. Maintenant, nous allons aller plus loin. Nous allons créer un décorateur plus flexible qui peut accepter des spécifications de type en tant qu'arguments. Cela signifie que nous pouvons définir les types que nous souhaitons pour chaque argument et la valeur de retour de manière plus explicite.
Compréhension de l'objectif
Notre objectif est de créer un décorateur @enforce(). Ce décorateur nous permettra de spécifier des contraintes de type en utilisant des arguments nommés. Voici un exemple de son fonctionnement :
@enforce(x=Integer, y=Integer, return_=Integer)
def add(x, y):
return x + y
Dans cet exemple, nous utilisons le décorateur @enforce pour spécifier que les arguments x et y de la fonction add doivent être de type Integer, et que la valeur de retour doit également être de type Integer. Ce décorateur se comportera de manière similaire à notre décorateur @validated précédent, mais il nous donne plus de contrôle sur les spécifications de type.
Création du décorateur enforce
- Tout d'abord, ouvrez le fichier
validate.pydans le WebIDE. Nous allons ajouter notre nouveau décorateur à ce fichier. Voici le code que nous allons ajouter :
from functools import wraps
class Integer:
@classmethod
def __instancecheck__(cls, x):
return isinstance(x, int)
def validated(func):
@wraps(func)
def wrapper(*args, **kwargs):
## Get function annotations
annotations = func.__annotations__
## Check arguments against annotations
for arg_name, arg_value in zip(func.__code__.co_varnames, args):
if arg_name in annotations and not isinstance(arg_value, annotations[arg_name]):
raise TypeError(f'Expected {arg_name} to be {annotations[arg_name].__name__}')
## Run the function and get the result
result = func(*args, **kwargs)
## Check the return value
if 'return' in annotations and not isinstance(result, annotations['return']):
raise TypeError(f'Expected return value to be {annotations["return"].__name__}')
return result
return wrapper
def enforce(**type_specs):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
## Check argument types
for arg_name, arg_value in zip(func.__code__.co_varnames, args):
if arg_name in type_specs and not isinstance(arg_value, type_specs[arg_name]):
raise TypeError(f'Expected {arg_name} to be {type_specs[arg_name].__name__}')
## Run the function and get the result
result = func(*args, **kwargs)
## Check the return value
if 'return_' in type_specs and not isinstance(result, type_specs['return_']):
raise TypeError(f'Expected return value to be {type_specs["return_"].__name__}')
return result
return wrapper
return decorator
Analysons ce que fait ce code. La classe Integer est utilisée pour définir un type personnalisé. Le décorateur validated vérifie les types des arguments de la fonction et de la valeur de retour en fonction des annotations de type de la fonction. Le décorateur enforce est le nouveau que nous créons. Il prend des arguments nommés qui spécifient les types pour chaque argument et la valeur de retour. À l'intérieur de la fonction wrapper du décorateur enforce, nous vérifions si les types des arguments et de la valeur de retour correspondent aux types spécifiés. Sinon, nous levons une erreur TypeError.
- Maintenant, testons notre nouveau décorateur
@enforce. Nous allons exécuter quelques cas de test pour voir s'il fonctionne comme prévu. Voici le code pour exécuter les tests :
cd ~/project
python3 -c "from validate import enforce, Integer
@enforce(x=Integer, y=Integer, return_=Integer)
def add(x, y):
return x + y
## This should work
print(add(2, 3))
## This should raise a TypeError
try:
print(add('2', 3))
except TypeError as e:
print(f'Error: {e}')
## This should raise a TypeError
try:
@enforce(x=Integer, y=Integer, return_=Integer)
def bad_add(x, y):
return str(x + y)
print(bad_add(2, 3))
except TypeError as e:
print(f'Error: {e}')"
Dans ce code de test, nous définissons d'abord une fonction add avec le décorateur @enforce. Nous appelons ensuite la fonction add avec des arguments valides, ce qui devrait fonctionner sans erreur. Ensuite, nous appelons la fonction add avec un argument invalide, ce qui devrait lever une erreur TypeError. Enfin, nous définissons une fonction bad_add qui retourne une valeur du mauvais type, ce qui devrait également lever une erreur TypeError.
Lorsque vous exécutez ce code de test, vous devriez voir une sortie similaire à ceci :
5
Error: Expected x to be Integer
Error: Expected return value to be Integer
Cette sortie montre que notre décorateur @enforce fonctionne correctement. Il lève une erreur TypeError lorsque les types des arguments ou de la valeur de retour ne correspondent pas aux types spécifiés.
Comparaison des deux approches
Les décorateurs @validated et @enforce atteignent le même objectif d'application de contraintes de type, mais ils le font de différentes manières.
Le décorateur
@validatedutilise les annotations de type intégrées à Python. Voici un exemple :@validated def add(x: Integer, y: Integer) -> Integer: return x + yAvec cette approche, nous spécifions les types directement dans la définition de la fonction en utilisant des annotations de type. C'est une fonctionnalité intégrée à Python, et elle offre un meilleur support dans les environnements de développement intégré (IDE). Les IDE peuvent utiliser ces annotations de type pour fournir des complétions de code, des vérifications de type et d'autres fonctionnalités utiles.
Le décorateur
@enforce, en revanche, utilise des arguments nommés pour spécifier les types. Voici un exemple :@enforce(x=Integer, y=Integer, return_=Integer) def add(x, y): return x + yCette approche est plus explicite car nous passons directement les spécifications de type en tant qu'arguments au décorateur. Elle peut être utile lorsque vous travaillez avec des bibliothèques qui reposent sur d'autres systèmes d'annotation.
Chaque approche a ses propres avantages. Les annotations de type sont une partie native de Python et offrent un meilleur support dans les IDE, tandis que l'approche @enforce nous donne plus de flexibilité et d'explicitation. Vous pouvez choisir l'approche qui convient le mieux à vos besoins en fonction du projet sur lequel vous travaillez.
Résumé
Dans ce laboratoire (lab), vous avez appris à créer et à utiliser efficacement les décorateurs. Vous avez appris à conserver les métadonnées des fonctions avec functools.wraps, à créer des décorateurs acceptant des paramètres, à gérer plusieurs décorateurs et à comprendre leur ordre d'application. Vous avez également appris à appliquer des décorateurs à différentes méthodes de classe et à créer un décorateur de vérification de type qui prend des arguments.
Ces modèles de décorateurs sont couramment utilisés dans les frameworks Python tels que Flask, Django et pytest. Maîtriser les décorateurs vous permettra d'écrire un code plus maintenable et réutilisable. Pour approfondir vos connaissances, vous pouvez explorer les gestionnaires de contexte (context managers), les décorateurs basés sur des classes, l'utilisation de décorateurs pour la mise en cache (caching) et la vérification avancée de type avec des décorateurs.