Comprendre les Décorateurs en Python

PythonBeginner
Pratiquer maintenant

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.

Ceci est un Guided Lab, qui fournit des instructions étape par étape pour vous aider à apprendre et à pratiquer. Suivez attentivement les instructions pour compléter chaque étape et acquérir une expérience pratique. Les données historiques montrent que c'est un laboratoire de niveau débutant avec un taux de réussite de 93%. Il a reçu un taux d'avis positifs de 100% de la part des apprenants.

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_activity qui accepte une fonction func comme argument.
  • À l'intérieur de log_activity, nous définissons une fonction imbriquée wrapper. Cette fonction contiendra le nouveau comportement. Elle affiche un message de journalisation, appelle la fonction originale func, puis affiche un autre message de journalisation.
  • La fonction log_activity retourne la fonction wrapper.
  • La syntaxe @log_activity au-dessus de la fonction greet est un raccourci pour greet = log_activity(greet). Elle applique notre décorateur à la fonction greet.

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 :

  1. Nous avons importé wraps depuis le module functools.
  2. Nous avons ajouté @wraps(func) juste au-dessus de la définition de notre fonction wrapper.

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 :

  • @property sur la méthode radius définit un "getter". Il est appelé lorsque vous accédez à c.radius.
  • @radius.setter définit un "setter" pour la propriété radius. Il est appelé lorsque vous assignez une valeur, comme c.radius = 10. Nous avons ajouté une validation ici pour empêcher les valeurs négatives.
  • La méthode area utilise également @property mais 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.