Apprendre les décorateurs de classe

Beginner

This tutorial is from open-source community. Access the source code

Introduction

Dans ce laboratoire, vous apprendrez les décorateurs de classe en Python et réviserez et étendrez les descripteurs Python. En combinant ces concepts, vous pouvez créer des structures de code puissantes et claires.

Dans ce laboratoire, vous vous appuierez sur les concepts de descripteurs précédents et les étendrez à l'aide de décorateurs de classe. Cette combinaison vous permet de créer un code plus propre, plus maintenable et doté de capacités de validation améliorées. Les fichiers à modifier sont validate.py et structure.py.

Implémentation de la vérification de type avec des descripteurs

Dans cette étape, nous allons créer une classe Stock qui utilise des descripteurs pour la vérification de type. Mais d'abord, comprenons ce que sont les descripteurs. Les descripteurs sont une fonctionnalité très puissante en Python. Ils vous donnent le contrôle sur la manière dont les attributs sont accédés dans les classes.

Les descripteurs sont des objets qui définissent comment les attributs sont accédés sur d'autres objets. Ils le font en implémentant des méthodes spéciales comme __get__, __set__, et __delete__. Ces méthodes permettent aux descripteurs de gérer la manière dont les attributs sont récupérés, définis et supprimés. Les descripteurs sont très utiles pour implémenter la validation, la vérification de type et les propriétés calculées. Par exemple, vous pouvez utiliser un descripteur pour vous assurer qu'un attribut est toujours un nombre positif ou une chaîne de caractères d'un certain format.

Le fichier validate.py contient déjà des classes de validation (String, PositiveInteger, PositiveFloat). Nous pouvons utiliser ces classes pour valider les attributs de notre classe Stock.

Maintenant, créons notre classe Stock avec des descripteurs.

  1. Tout d'abord, ouvrez le fichier stock.py dans votre éditeur.

  2. Une fois le fichier ouvert, remplacez le contenu de l'espace réservé par le code suivant :

## stock.py

from structure import Structure
from validate import String, PositiveInteger, PositiveFloat

class Stock(Structure):
    _fields = ('name', 'shares', 'price')
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

## Create an __init__ method based on _fields
Stock.create_init()

Analysons ce que fait ce code. Le tuple _fields définit les attributs de la classe Stock. Ce sont les noms des attributs que nos objets Stock auront.

Les attributs name, shares et price sont définis comme des objets descripteurs. Le descripteur String() garantit que l'attribut name est une chaîne de caractères. Le descripteur PositiveInteger() s'assure que l'attribut shares est un entier positif. Et le descripteur PositiveFloat() garantit que l'attribut price est un nombre à virgule flottante positif.

La propriété cost est une propriété calculée. Elle calcule le coût total du stock en fonction du nombre d'actions et du prix par action.

La méthode sell est utilisée pour réduire le nombre d'actions. Lorsque vous appelez cette méthode avec un nombre d'actions à vendre, elle soustrait ce nombre de l'attribut shares.

La ligne Stock.create_init() crée dynamiquement une méthode __init__ pour notre classe. Cette méthode nous permet de créer des objets Stock en passant les valeurs des attributs name, shares et price.

  1. Après avoir ajouté le code, enregistrez le fichier. Cela garantira que vos modifications sont enregistrées et peuvent être utilisées lorsque vous exécutez les tests.

  2. Exécutons maintenant les tests pour vérifier votre implémentation. Tout d'abord, changez de répertoire pour vous rendre dans le répertoire ~/project en exécutant la commande suivante :

cd ~/project

Ensuite, exécutez les tests en utilisant la commande suivante :

python3 teststock.py

Si votre implémentation est correcte, vous devriez voir une sortie similaire à celle-ci :

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK

Cette sortie signifie que tous les tests sont réussis. Les descripteurs valident avec succès les types de chaque attribut !

Essayons de créer un objet Stock dans l'interpréteur Python. Tout d'abord, assurez-vous d'être dans le répertoire ~/project. Ensuite, exécutez la commande suivante :

cd ~/project
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print(s); print(f'Cost: {s.cost}')"

Vous devriez voir la sortie suivante :

Stock('GOOG', 100, 490.1)
Cost: 49010.0

Vous avez implémenté avec succès des descripteurs pour la vérification de type ! Améliorons maintenant ce code.

Création d'un décorateur de classe pour la validation

Dans l'étape précédente, notre implémentation a fonctionné, mais il y avait une redondance. Nous devions spécifier à la fois le tuple _fields et les attributs descripteurs. Ce n'est pas très efficace, et nous pouvons l'améliorer. En Python, les décorateurs de classe sont un outil puissant qui peut nous aider à simplifier ce processus. Un décorateur de classe est une fonction qui prend une classe comme argument, la modifie d'une certaine manière, puis retourne la classe modifiée. En utilisant un décorateur de classe, nous pouvons extraire automatiquement les informations de champ des descripteurs, ce qui rendra notre code plus propre et plus maintenable.

Créons un décorateur de classe pour simplifier notre code. Voici les étapes à suivre :

  1. Tout d'abord, ouvrez le fichier structure.py dans votre éditeur.

  2. Ensuite, ajoutez le code suivant en haut du fichier structure.py, juste après les instructions d'importation. Ce code définit notre décorateur de classe :

from validate import Validator

def validate_attributes(cls):
    """
    Class decorator that extracts Validator instances
    and builds the _fields list automatically
    """
    validators = []
    for name, val in vars(cls).items():
        if isinstance(val, Validator):
            validators.append(val)

    ## Set _fields based on validator names
    cls._fields = [val.name for val in validators]

    ## Create initialization method
    cls.create_init()

    return cls

Analysons ce que fait ce décorateur :

  • Il crée d'abord une liste vide appelée validators. Ensuite, il parcourt tous les attributs de la classe en utilisant vars(cls).items(). Si un attribut est une instance de la classe Validator, il ajoute cet attribut à la liste validators.
  • Après cela, il définit l'attribut _fields de la classe. Il crée une liste de noms à partir des validateurs dans la liste validators et l'assigne à cls._fields.
  • Enfin, il appelle la méthode create_init() de la classe pour générer la méthode __init__, puis retourne la classe modifiée.
  1. Une fois que vous avez ajouté le code, enregistrez le fichier structure.py. L'enregistrement du fichier garantit que vos modifications sont conservées.

  2. Maintenant, nous devons modifier notre fichier stock.py pour utiliser ce nouveau décorateur. Ouvrez le fichier stock.py dans votre éditeur.

  3. Mettez à jour le fichier stock.py pour utiliser le décorateur validate_attributes. Remplacez le code existant par le suivant :

## stock.py

from structure import Structure, validate_attributes
from validate import String, PositiveInteger, PositiveFloat

@validate_attributes
class Stock(Structure):
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

Remarquez les changements que nous avons apportés :

  • Nous avons ajouté le décorateur @validate_attributes juste au-dessus de la définition de la classe Stock. Cela indique à Python d'appliquer le décorateur validate_attributes à la classe Stock.
  • Nous avons supprimé la déclaration explicite de _fields car le décorateur s'en chargera automatiquement.
  • Nous avons également supprimé l'appel à Stock.create_init() car le décorateur s'occupe de la création de la méthode __init__.

En conséquence, la classe est maintenant plus simple et plus propre. Le décorateur prend en charge tous les détails que nous gérions auparavant manuellement.

  1. Après avoir effectué ces modifications, nous devons vérifier que tout fonctionne toujours comme prévu. Exécutez à nouveau les tests en utilisant les commandes suivantes :
cd ~/project
python3 teststock.py

Si tout fonctionne correctement, vous devriez voir la sortie suivante :

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK

Cette sortie indique que tous les tests ont été réussis.

Testons également notre classe Stock de manière interactive. Exécutez la commande suivante dans le terminal :

cd ~/project
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print(s); print(f'Cost: {s.cost}')"

Vous devriez voir la sortie suivante :

Stock('GOOG', 100, 490.1)
Cost: 49010.0

Excellent ! Vous avez implémenté avec succès un décorateur de classe qui simplifie notre code en gérant automatiquement les déclarations de champs et l'initialisation. Cela rend notre code plus efficace et plus facile à maintenir.

Application des décorateurs via l'héritage

Dans l'étape 2, nous avons créé un décorateur de classe qui simplifie notre code. Un décorateur de classe est un type spécial de fonction qui prend une classe comme argument et retourne une classe modifiée. C'est un outil utile en Python pour ajouter des fonctionnalités aux classes sans modifier leur code d'origine. Cependant, nous devons toujours appliquer explicitement le décorateur @validate_attributes à chaque classe. Cela signifie que chaque fois que nous créons une nouvelle classe qui nécessite une validation, nous devons nous souvenir d'ajouter ce décorateur, ce qui peut être un peu fastidieux.

Nous pouvons améliorer cela davantage en appliquant le décorateur automatiquement via l'héritage. L'héritage est un concept fondamental en programmation orientée objet où une sous-classe peut hériter d'attributs et de méthodes d'une classe parente. La méthode __init_subclass__ de Python a été introduite dans Python 3.6 pour permettre aux classes parentes de personnaliser l'initialisation des sous-classes. Cela signifie que lorsqu'une sous-classe est créée, la classe parente peut effectuer certaines actions sur celle-ci. Nous pouvons utiliser cette fonctionnalité pour appliquer automatiquement notre décorateur à toute classe qui hérite de Structure.

Implémentons cela :

  1. Ouvrez le fichier structure.py dans votre éditeur. Ce fichier contient la définition de la classe Structure, et nous allons le modifier pour utiliser la méthode __init_subclass__.

  2. Ajoutez la méthode __init_subclass__ à la classe Structure :

class Structure:
    _fields = ()
    _types = ()

    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError(f'Expected {len(self._fields)} arguments')
        for name, val in zip(self._fields, args):
            setattr(self, name, val)

    def __repr__(self):
        values = ', '.join(repr(getattr(self, name)) for name in self._fields)
        return f'{type(self).__name__}({values})'

    @classmethod
    def create_init(cls):
        '''
        Create an __init__ method from _fields
        '''
        body = 'def __init__(self, %s):\n' % ', '.join(cls._fields)
        for name in cls._fields:
            body += f'    self.{name} = {name}\n'

        ## Execute the function creation code
        namespace = {}
        exec(body, namespace)
        setattr(cls, '__init__', namespace['__init__'])

    @classmethod
    def __init_subclass__(cls):
        validate_attributes(cls)

La méthode __init_subclass__ est une méthode de classe, ce qui signifie qu'elle peut être appelée sur la classe elle-même plutôt que sur une instance de la classe. Lorsqu'une sous-classe de Structure est créée, cette méthode sera automatiquement appelée. À l'intérieur de cette méthode, nous appelons le décorateur validate_attributes sur la sous-classe cls. De cette façon, chaque sous-classe de Structure aura automatiquement le comportement de validation.

  1. Enregistrez le fichier.

Après avoir apporté des modifications au fichier structure.py, nous devons l'enregistrer afin que les modifications soient appliquées.

  1. Maintenant, mettons à jour notre fichier stock.py pour tirer parti de cette nouvelle fonctionnalité. Ouvrez le fichier stock.py dans votre éditeur pour le modifier. Ce fichier contient la définition de la classe Stock, et nous allons la faire hériter de la classe Structure pour utiliser l'application automatique du décorateur.

  2. Modifiez le fichier stock.py pour supprimer le décorateur explicite :

## stock.py

from structure import Structure
from validate import String, PositiveInteger, PositiveFloat

class Stock(Structure):
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

Notez ce que nous avons fait :

  • Nous avons supprimé l'importation de validate_attributes car nous n'avons plus besoin de l'importer explicitement puisque le décorateur est appliqué automatiquement via l'héritage.
  • Nous avons supprimé le décorateur @validate_attributes car la méthode __init_subclass__ dans la classe Structure s'en chargera.
  • Le code repose désormais uniquement sur l'héritage de Structure pour obtenir le comportement de validation.
  1. Exécutez à nouveau les tests pour vérifier que tout fonctionne toujours :
cd ~/project
python3 teststock.py

L'exécution des tests est importante pour s'assurer que nos modifications n'ont rien cassé. Si tous les tests passent, cela signifie que l'application automatique du décorateur via l'héritage fonctionne correctement.

Vous devriez voir tous les tests passer :

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK

Testons à nouveau notre classe Stock pour nous assurer qu'elle fonctionne comme prévu :

cd ~/project
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print(s); print(f'Cost: {s.cost}')"

Cette commande crée une instance de la classe Stock et imprime sa représentation ainsi que son coût. Si la sortie est conforme aux attentes, cela signifie que la classe Stock fonctionne correctement avec l'application automatique du décorateur.

Sortie :

Stock('GOOG', 100, 490.1)
Cost: 49010.0

Cette implémentation est encore plus propre ! En utilisant __init_subclass__, nous avons éliminé la nécessité d'appliquer explicitement des décorateurs. Toute classe qui hérite de Structure obtient automatiquement le comportement de validation.

Ajout de la fonctionnalité de conversion de ligne

En programmation, il est souvent utile de créer des instances d'une classe à partir de lignes de données, en particulier lorsque l'on traite des données provenant de sources telles que des fichiers CSV. Dans cette section, nous allons ajouter la capacité de créer des instances de la classe Structure à partir de lignes de données. Pour ce faire, nous implémenterons une méthode de classe from_row dans la classe Structure.

  1. Tout d'abord, ouvrez le fichier structure.py dans votre éditeur. C'est là que nous apporterons nos modifications de code.

  2. Ensuite, nous allons modifier la fonction validate_attributes. Cette fonction est un décorateur de classe qui extrait les instances de Validator et construit automatiquement les listes _fields et _types. Nous la mettrons à jour pour qu'elle collecte également les informations de type.

def validate_attributes(cls):
    """
    Class decorator that extracts Validator instances
    and builds the _fields and _types lists automatically
    """
    validators = []
    for name, val in vars(cls).items():
        if isinstance(val, Validator):
            validators.append(val)

    ## Set _fields based on validator names
    cls._fields = [val.name for val in validators]

    ## Set _types based on validator expected_types
    cls._types = [getattr(val, 'expected_type', lambda x: x) for val in validators]

    ## Create initialization method
    cls.create_init()

    return cls

Dans cette fonction mise à jour, nous collectons l'attribut expected_type de chaque validateur et le stockons dans la variable de classe _types. Cela sera utile plus tard lorsque nous convertirons les données des lignes dans les types corrects.

  1. Maintenant, nous allons ajouter la méthode de classe from_row à la classe Structure. Cette méthode nous permettra de créer une instance de la classe à partir d'une ligne de données, qui peut être une liste ou un tuple.
@classmethod
def from_row(cls, row):
    """
    Create an instance from a data row (list or tuple)
    """
    rowdata = [func(val) for func, val in zip(cls._types, row)]
    return cls(*rowdata)

Voici comment fonctionne cette méthode :

  • Elle prend une ligne de données, qui peut être sous forme de liste ou de tuple.
  • Elle convertit chaque valeur de la ligne dans le type attendu en utilisant la fonction correspondante de la liste _types.
  • Elle crée ensuite et retourne une nouvelle instance de la classe en utilisant les valeurs converties.
  1. Après avoir apporté ces modifications, enregistrez le fichier structure.py. Cela garantit que vos modifications de code sont conservées.

  2. Testons notre méthode from_row pour nous assurer qu'elle fonctionne comme prévu. Nous allons créer un test simple en utilisant la classe Stock. Exécutez la commande suivante dans votre terminal :

cd ~/project
python3 -c "from stock import Stock; s = Stock.from_row(['GOOG', '100', '490.1']); print(s); print(f'Cost: {s.cost}')"

Vous devriez voir une sortie similaire à celle-ci :

Stock('GOOG', 100, 490.1)
Cost: 49010.0

Notez que les valeurs de chaîne '100' et '490.1' ont été automatiquement converties dans les types corrects (entier et flottant). Cela montre que notre méthode from_row fonctionne correctement.

  1. Enfin, essayons de lire des données à partir d'un fichier CSV en utilisant notre module reader.py. Exécutez la commande suivante dans votre terminal :
cd ~/project
python3 -c "from stock import Stock; import reader; portfolio = reader.read_csv_as_instances('portfolio.csv', Stock); print(portfolio); print(f'Total value: {sum(s.cost for s in portfolio)}')"

Vous devriez voir une sortie affichant les actions du fichier CSV :

[Stock('GOOG', 100, 490.1), Stock('AAPL', 50, 545.75), Stock('MSFT', 200, 30.47)]
Total value: 82391.5

La méthode from_row nous permet de convertir facilement des données CSV en instances de la classe Stock. Lorsqu'elle est combinée avec la fonction read_csv_as_instances, nous avons un moyen puissant de charger et de travailler avec des données structurées.

Ajout de la validation des arguments de méthode

En Python, la validation des données est une partie importante de l'écriture de code robuste. Dans cette section, nous allons pousser notre validation un peu plus loin en validant automatiquement les arguments des méthodes. Le fichier validate.py inclut déjà un décorateur @validated. Un décorateur en Python est une fonction spéciale qui peut modifier une autre fonction. Le décorateur @validated ici peut vérifier les arguments de la fonction par rapport à leurs annotations. Les annotations en Python sont un moyen d'ajouter des métadonnées aux paramètres et aux valeurs de retour des fonctions.

Modifions notre code pour appliquer ce décorateur aux méthodes avec des annotations :

  1. Tout d'abord, nous devons comprendre comment fonctionne le décorateur validated. Ouvrez le fichier validate.py dans votre éditeur pour l'examiner.

Le décorateur validated utilise les annotations de fonction pour valider les arguments. Avant de permettre à la fonction de s'exécuter, il crée une instance de la classe de validation pour chaque paramètre annoté et appelle la méthode validate pour vérifier l'argument. Par exemple, si un argument est annoté avec PositiveInteger, le décorateur créera une instance de PositiveInteger et validera que la valeur passée est bien un entier positif. Si la validation échoue, il collecte toutes les erreurs et lève une TypeError avec des messages d'erreur détaillés.

  1. Maintenant, nous allons modifier la fonction validate_attributes dans structure.py pour encapsuler les méthodes annotées avec le décorateur validated. Cela signifie que toute méthode avec des annotations dans la classe aura ses arguments automatiquement validés. Ouvrez le fichier structure.py dans votre éditeur.

  2. Mettez à jour la fonction validate_attributes :

def validate_attributes(cls):
    """
    Class decorator that:
    1. Extracts Validator instances and builds _fields and _types lists
    2. Applies @validated decorator to methods with annotations
    """
    ## Import the validated decorator
    from validate import validated

    ## Process validator descriptors
    validators = []
    for name, val in vars(cls).items():
        if isinstance(val, Validator):
            validators.append(val)

    ## Set _fields based on validator names
    cls._fields = [val.name for val in validators]

    ## Set _types based on validator expected_types
    cls._types = [getattr(val, 'expected_type', lambda x: x) for val in validators]

    ## Apply @validated decorator to methods with annotations
    for name, val in vars(cls).items():
        if callable(val) and hasattr(val, '__annotations__'):
            setattr(cls, name, validated(val))

    ## Create initialization method
    cls.create_init()

    return cls

Cette fonction mise à jour fait maintenant ce qui suit :

  1. Elle traite les descripteurs de validation comme auparavant. Les descripteurs de validation sont utilisés pour définir les règles de validation des attributs de classe.

  2. Elle trouve toutes les méthodes avec des annotations dans la classe. Les annotations sont ajoutées aux paramètres de méthode pour spécifier le type attendu de l'argument.

  3. Elle applique le décorateur @validated à ces méthodes. Cela garantit que les arguments passés à ces méthodes sont validés conformément à leurs annotations.

  4. Enregistrez le fichier après avoir effectué ces modifications. L'enregistrement du fichier est important car il garantit que nos modifications sont stockées et peuvent être utilisées ultérieurement.

  5. Maintenant, mettons à jour la méthode sell dans la classe Stock pour inclure une annotation. Les annotations aident à spécifier le type attendu de l'argument, qui sera utilisé par le décorateur @validated pour la validation. Ouvrez le fichier stock.py dans votre éditeur.

  6. Modifiez la méthode sell pour inclure une annotation de type :

## stock.py

from structure import Structure
from validate import String, PositiveInteger, PositiveFloat

class Stock(Structure):
    name = String()
    shares = PositiveInteger()
    price = PositiveFloat()

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares: PositiveInteger):
        self.shares -= nshares

Le changement important est l'ajout de : PositiveInteger au paramètre nshares. Cela indique à Python (et à notre décorateur @validated) de valider cet argument en utilisant le validateur PositiveInteger. Ainsi, lorsque nous appelons la méthode sell, l'argument nshares doit être un entier positif.

  1. Exécutez à nouveau les tests pour vérifier que tout fonctionne toujours. L'exécution des tests est un bon moyen de s'assurer que nos modifications n'ont pas cassé de fonctionnalités existantes.
cd ~/project
python3 teststock.py

Vous devriez voir tous les tests passer :

.........
----------------------------------------------------------------------
Ran 9 tests in 0.001s

OK
  1. Testons notre nouvelle validation d'arguments. Nous allons essayer d'appeler la méthode sell avec des arguments valides et invalides pour voir si la validation fonctionne comme prévu.
cd ~/project
python3 -c "
from stock import Stock
s = Stock('GOOG', 100, 490.1)
s.sell(25)
print(s)
try:
    s.sell(-25)
except Exception as e:
    print(f'Error: {e}')
"

Vous devriez voir une sortie similaire à :

Stock('GOOG', 75, 490.1)
Error: Bad Arguments
  nshares: nshares must be >= 0

Cela montre que notre validation des arguments de méthode fonctionne ! Le premier appel à sell(25) réussit car 25 est un entier positif. Mais le second appel à sell(-25) échoue car -25 n'est pas un entier positif.

Vous avez maintenant implémenté un système complet pour :

  1. Valider les attributs de classe à l'aide de descripteurs. Les descripteurs sont utilisés pour définir les règles de validation des attributs de classe.
  2. Collecter automatiquement les informations sur les champs à l'aide de décorateurs de classe. Les décorateurs de classe peuvent modifier le comportement d'une classe, comme la collecte d'informations sur les champs.
  3. Convertir les données de ligne en instances. Ceci est utile lorsque l'on travaille avec des données provenant de sources externes.
  4. Valider les arguments de méthode à l'aide d'annotations. Les annotations aident à spécifier le type attendu de l'argument pour la validation.

Cela démontre la puissance de la combinaison des descripteurs et des décorateurs en Python pour créer des classes expressives et auto-validantes.

Résumé

Dans ce laboratoire, vous avez appris à combiner de puissantes fonctionnalités Python pour créer du code propre et auto-validant. Vous avez maîtrisé des concepts clés tels que l'utilisation de descripteurs pour la validation d'attributs, la création de décorateurs de classe pour l'automatisation de la génération de code et l'application automatique de décorateurs par héritage.

Ces techniques sont de puissants outils pour créer du code Python robuste et maintenable. Elles vous permettent d'exprimer clairement les exigences de validation et de les appliquer dans l'ensemble de votre base de code. Vous pouvez désormais appliquer ces modèles dans vos propres projets Python pour améliorer la qualité du code et réduire le code répétitif.