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.
Tout d'abord, ouvrez le fichier
stock.pydans votre éditeur.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.
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.
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
~/projecten 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 :
Tout d'abord, ouvrez le fichier
structure.pydans votre éditeur.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 utilisantvars(cls).items(). Si un attribut est une instance de la classeValidator, il ajoute cet attribut à la listevalidators. - Après cela, il définit l'attribut
_fieldsde la classe. Il crée une liste de noms à partir des validateurs dans la listevalidatorset 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.
Une fois que vous avez ajouté le code, enregistrez le fichier
structure.py. L'enregistrement du fichier garantit que vos modifications sont conservées.Maintenant, nous devons modifier notre fichier
stock.pypour utiliser ce nouveau décorateur. Ouvrez le fichierstock.pydans votre éditeur.Mettez à jour le fichier
stock.pypour utiliser le décorateurvalidate_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_attributesjuste au-dessus de la définition de la classeStock. Cela indique à Python d'appliquer le décorateurvalidate_attributesà la classeStock. - Nous avons supprimé la déclaration explicite de
_fieldscar 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.
- 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 :
Ouvrez le fichier
structure.pydans votre éditeur. Ce fichier contient la définition de la classeStructure, et nous allons le modifier pour utiliser la méthode__init_subclass__.Ajoutez la méthode
__init_subclass__à la classeStructure:
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.
- Enregistrez le fichier.
Après avoir apporté des modifications au fichier structure.py, nous devons l'enregistrer afin que les modifications soient appliquées.
Maintenant, mettons à jour notre fichier
stock.pypour tirer parti de cette nouvelle fonctionnalité. Ouvrez le fichierstock.pydans votre éditeur pour le modifier. Ce fichier contient la définition de la classeStock, et nous allons la faire hériter de la classeStructurepour utiliser l'application automatique du décorateur.Modifiez le fichier
stock.pypour 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_attributescar 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_attributescar la méthode__init_subclass__dans la classeStructures'en chargera. - Le code repose désormais uniquement sur l'héritage de
Structurepour obtenir le comportement de validation.
- 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.
Tout d'abord, ouvrez le fichier
structure.pydans votre éditeur. C'est là que nous apporterons nos modifications de code.Ensuite, nous allons modifier la fonction
validate_attributes. Cette fonction est un décorateur de classe qui extrait les instances deValidatoret construit automatiquement les listes_fieldset_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.
- Maintenant, nous allons ajouter la méthode de classe
from_rowà la classeStructure. 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.
Après avoir apporté ces modifications, enregistrez le fichier
structure.py. Cela garantit que vos modifications de code sont conservées.Testons notre méthode
from_rowpour nous assurer qu'elle fonctionne comme prévu. Nous allons créer un test simple en utilisant la classeStock. 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.
- 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 :
- Tout d'abord, nous devons comprendre comment fonctionne le décorateur
validated. Ouvrez le fichiervalidate.pydans 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.
Maintenant, nous allons modifier la fonction
validate_attributesdansstructure.pypour encapsuler les méthodes annotées avec le décorateurvalidated. Cela signifie que toute méthode avec des annotations dans la classe aura ses arguments automatiquement validés. Ouvrez le fichierstructure.pydans votre éditeur.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 :
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.
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.
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.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.
Maintenant, mettons à jour la méthode
selldans la classeStockpour inclure une annotation. Les annotations aident à spécifier le type attendu de l'argument, qui sera utilisé par le décorateur@validatedpour la validation. Ouvrez le fichierstock.pydans votre éditeur.Modifiez la méthode
sellpour 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.
- 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
- Testons notre nouvelle validation d'arguments. Nous allons essayer d'appeler la méthode
sellavec 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 :
- 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.
- 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.
- Convertir les données de ligne en instances. Ceci est utile lorsque l'on travaille avec des données provenant de sources externes.
- 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.