Introduction
Dans ce laboratoire, vous acquerrez une compréhension complète des décorateurs (decorators) en Python, une fonctionnalité puissante permettant de modifier ou d'améliorer des fonctions et des méthodes. Nous commencerons par introduire le concept fondamental des décorateurs et explorerons leur utilisation de base à travers des exemples pratiques.
En nous appuyant sur ces bases, vous apprendrez à utiliser efficacement functools.wraps pour préserver les métadonnées importantes de la fonction décorée. Nous aborderons ensuite des décorateurs spécifiques tels que le décorateur property, en comprenant son rôle dans la gestion de l'accès aux attributs. Enfin, le laboratoire clarifiera les distinctions entre les méthodes d'instance, les méthodes de classe et les méthodes statiques, en démontrant comment les décorateurs sont utilisés dans ces contextes pour contrôler le comportement des méthodes au sein des classes.
Comprendre les Décorateurs de Base
Dans cette étape, nous allons introduire le concept de décorateurs et leur utilisation de base. Un décorateur est une fonction qui prend une autre fonction comme argument, lui ajoute une certaine fonctionnalité et retourne une autre fonction, le tout sans modifier le code source de la fonction originale.
Tout d'abord, localisez le fichier decorator_basics.py dans l'explorateur de fichiers sur le côté gauche du WebIDE. Double-cliquez pour l'ouvrir. Nous allons écrire notre premier décorateur dans ce fichier.
Copiez et collez le code suivant dans decorator_basics.py:
import datetime
def log_activity(func):
"""A simple decorator to log function calls."""
def wrapper(*args, **kwargs):
print(f"Calling function '{func.__name__}' at {datetime.datetime.now()}")
result = func(*args, **kwargs)
print(f"Function '{func.__name__}' finished.")
return result
return wrapper
@log_activity
def greet(name):
"""A simple function to greet someone."""
print(f"Hello, {name}!")
## Call the decorated function
greet("Alice")
## Let's inspect the function's metadata
print(f"\nFunction name: {greet.__name__}")
print(f"Function docstring: {greet.__doc__}")
Décortiquons ce code :
- Nous définissons une fonction décoratrice
log_activityqui accepte une fonctionfunccomme argument. - À l'intérieur de
log_activity, nous définissons une fonction imbriquéewrapper. Cette fonction contiendra le nouveau comportement. Elle affiche un message de journalisation, appelle la fonction originalefunc, puis affiche un autre message de journalisation. - La fonction
log_activityretourne la fonctionwrapper. - La syntaxe
@log_activityau-dessus de la fonctiongreetest un raccourci pourgreet = log_activity(greet). Elle applique notre décorateur à la fonctiongreet.
Maintenant, enregistrez le fichier (vous pouvez utiliser Ctrl+S ou Cmd+S). Pour exécuter le script, ouvrez le terminal intégré en bas du WebIDE et exécutez la commande suivante :
python ~/project/decorator_basics.py
Vous verrez la sortie suivante. Notez que la date et l'heure varieront.
Calling function 'greet' at 2023-10-27 10:30:00.123456
Hello, Alice!
Function 'greet' finished.
Function name: wrapper
Function docstring: None
Remarquez deux choses dans la sortie. Premièrement, notre fonction greet est maintenant enveloppée par les messages de journalisation. Deuxièmement, le nom et la docstring de la fonction ont été remplacés par ceux de la fonction wrapper. Cela peut poser problème pour le débogage et l'introspection. Dans l'étape suivante, nous apprendrons à corriger cela.
Préserver les Métadonnées de Fonction avec functools.wraps
Dans l'étape précédente, nous avons observé que décorer une fonction remplace ses métadonnées originales (comme __name__ et __doc__) par celles de la fonction enveloppe (wrapper). Le module functools de Python fournit une solution à ce problème : le décorateur wraps.
Le décorateur wraps est utilisé à l'intérieur de votre propre décorateur pour copier les métadonnées de la fonction originale vers la fonction enveloppe.
Modifions notre code dans decorator_basics.py. Ouvrez le fichier dans le WebIDE et mettez-le à jour pour utiliser functools.wraps.
import datetime
from functools import wraps
def log_activity(func):
"""A simple decorator to log function calls."""
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling function '{func.__name__}' at {datetime.datetime.now()}")
result = func(*args, **kwargs)
print(f"Function '{func.__name__}' finished.")
return result
return wrapper
@log_activity
def greet(name):
"""A simple function to greet someone."""
print(f"Hello, {name}!")
## Call the decorated function
greet("Alice")
## Let's inspect the function's metadata again
print(f"\nFunction name: {greet.__name__}")
print(f"Function docstring: {greet.__doc__}")
Les seuls changements sont :
- Nous avons importé
wrapsdepuis le modulefunctools. - Nous avons ajouté
@wraps(func)juste au-dessus de la définition de notre fonctionwrapper.
Enregistrez le fichier et exécutez-le à nouveau depuis le terminal :
python ~/project/decorator_basics.py
Maintenant, la sortie sera différente :
Calling function 'greet' at 2023-10-27 10:35:00.543210
Hello, Alice!
Function 'greet' finished.
Function name: greet
Function docstring: A simple function to greet someone.
Comme vous pouvez le constater, le nom de la fonction est correctement rapporté comme greet, et sa docstring originale est préservée. L'utilisation de functools.wraps est une bonne pratique qui rend vos décorateurs plus robustes et professionnels.
Implémenter des Attributs Gérés avec @property
Python fournit plusieurs décorateurs intégrés. L'un des plus utiles est @property, qui permet de transformer une méthode de classe en un "attribut géré" (managed attribute). Ceci est idéal pour ajouter une logique telle que la validation ou le calcul à l'accès aux attributs sans modifier la manière dont les utilisateurs interagissent avec votre classe.
Explorons cela en créant une classe Circle. Ouvrez le fichier property_decorator.py dans l'explorateur de fichiers.
Copiez et collez le code suivant dans property_decorator.py:
import math
class Circle:
def __init__(self, radius):
## The actual value is stored in a "private" attribute
self._radius = radius
@property
def radius(self):
"""The radius property."""
print("Getting radius...")
return self._radius
@radius.setter
def radius(self, value):
"""The radius setter with validation."""
print(f"Setting radius to {value}...")
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@property
def area(self):
"""A read-only computed property for the area."""
print("Calculating area...")
return math.pi * self._radius ** 2
## --- Let's test our Circle class ---
c = Circle(5)
## Access the radius like a normal attribute (triggers the getter)
print(f"Initial radius: {c.radius}\n")
## Change the radius (triggers the setter)
c.radius = 10
print(f"New radius: {c.radius}\n")
## Access the computed area property
print(f"Circle area: {c.area:.2f}\n")
## Try to set an invalid radius (triggers the setter's validation)
try:
c.radius = -2
except ValueError as e:
print(f"Error: {e}")
Dans ce code :
@propertysur la méthoderadiusdéfinit un "getter". Il est appelé lorsque vous accédez àc.radius.@radius.setterdéfinit un "setter" pour la propriétéradius. Il est appelé lorsque vous assignez une valeur, commec.radius = 10. Nous avons ajouté une validation ici pour empêcher les valeurs négatives.- La méthode
areautilise également@propertymais n'a pas de setter, ce qui en fait un attribut en lecture seule (read-only). Sa valeur est calculée chaque fois qu'elle est accédée.
Enregistrez le fichier et exécutez-le depuis le terminal :
python ~/project/property_decorator.py
Vous devriez voir la sortie suivante, démontrant comment la logique du getter, du setter et de la validation est automatiquement invoquée :
Getting radius...
Initial radius: 5
Setting radius to 10...
Getting radius...
New radius: 10
Calculating area...
Circle area: 314.16
Setting radius to -2...
Error: Radius cannot be negative
Différencier les Méthodes d'Instance, de Classe et Statiques
Dans les classes Python, les méthodes peuvent être liées à une instance, à la classe, ou ne pas être liées du tout. Des décorateurs sont utilisés pour définir ces différents types de méthodes.
- Méthodes d'Instance (Instance Methods) : Le type par défaut. Elles reçoivent l'instance comme premier argument, conventionnellement nommé
self. Elles opèrent sur des données spécifiques à l'instance. - Méthodes de Classe (Class Methods) : Marquées avec
@classmethod. Elles reçoivent la classe comme premier argument, conventionnellement nommécls. Elles opèrent sur des données au niveau de la classe et sont souvent utilisées comme constructeurs alternatifs. - Méthodes Statiques (Static Methods) : Marquées avec
@staticmethod. Elles ne reçoivent aucun premier argument spécial. Ce sont essentiellement des fonctions régulières nommées dans l'espace de noms d'une classe et ne peuvent accéder ni à l'état de l'instance ni à celui de la classe.
Voyons les trois en action. Ouvrez le fichier class_methods.py dans l'explorateur de fichiers.
Copiez et collez le code suivant dans class_methods.py:
class MyClass:
class_variable = "I am a class variable"
def __init__(self, instance_variable):
self.instance_variable = instance_variable
## 1. Instance Method
def instance_method(self):
print("\n--- Calling Instance Method ---")
print(f"Can access instance data: self.instance_variable = '{self.instance_variable}'")
print(f"Can access class data: self.class_variable = '{self.class_variable}'")
## 2. Class Method
@classmethod
def class_method(cls):
print("\n--- Calling Class Method ---")
print(f"Can access class data: cls.class_variable = '{cls.class_variable}'")
## Note: Cannot access instance_variable without an instance
print("Cannot access instance data directly.")
## 3. Static Method
@staticmethod
def static_method(a, b):
print("\n--- Calling Static Method ---")
print("Cannot access instance or class data directly.")
print(f"Just a utility function: {a} + {b} = {a + b}")
## --- Let's test the methods ---
## Create an instance of the class
my_instance = MyClass("I am an instance variable")
## Call the instance method (requires an instance)
my_instance.instance_method()
## Call the class method (can be called on the class or an instance)
MyClass.class_method()
my_instance.class_method() ## Also works
## Call the static method (can be called on the class or an instance)
MyClass.static_method(10, 5)
my_instance.static_method(20, 8) ## Also works
Enregistrez le fichier et exécutez-le depuis le terminal :
python ~/project/class_methods.py
Examinez attentivement la sortie. Elle démontre clairement les capacités et les limitations de chaque type de méthode.
--- Calling Instance Method ---
Can access instance data: self.instance_variable = 'I am an instance variable'
Can access class data: self.class_variable = 'I am a class variable'
--- Calling Class Method ---
Can access class data: cls.class_variable = 'I am a class variable'
Cannot access instance data directly.
--- Calling Class Method ---
Can access class data: cls.class_variable = 'I am a class variable'
Cannot access instance data directly.
--- Calling Static Method ---
Cannot access instance or class data directly.
Just a utility function: 10 + 5 = 15
--- Calling Static Method ---
Cannot access instance or class data directly.
Just a utility function: 20 + 8 = 28
Cet exemple fournit une référence claire pour savoir quand utiliser chaque type de méthode en fonction de la nécessité d'accéder à l'état de l'instance, à l'état de la classe, ou à aucun des deux.
Résumé
Dans ce laboratoire, vous avez acquis une compréhension pratique des décorateurs (decorators) en Python. Vous avez commencé par apprendre à créer et appliquer un décorateur de base pour ajouter des fonctionnalités à une fonction. Vous avez ensuite constaté l'importance d'utiliser functools.wraps pour préserver les métadonnées de la fonction originale, une bonne pratique cruciale pour écrire des décorateurs propres et maintenables.
De plus, vous avez exploré des décorateurs intégrés puissants. Vous avez appris à utiliser le décorateur @property pour créer des attributs gérés avec une logique de getter et de setter personnalisée, permettant des fonctionnalités telles que la validation des entrées. Enfin, vous avez fait la distinction entre les méthodes d'instance, les méthodes de classe (@classmethod) et les méthodes statiques (@staticmethod), comprenant comment chacune sert un objectif différent au sein d'une structure de classe en fonction de son accès à l'état de l'instance et de la classe.



