Apprenez-en sur les descripteurs

PythonPythonBeginner
Pratiquer maintenant

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

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Dans ce laboratoire, vous allez apprendre à connaître les descripteurs (descriptors) en Python, un mécanisme puissant permettant de personnaliser l'accès aux attributs des objets. Les descripteurs vous permettent de définir la manière dont les attributs sont accédés, définis et supprimés, vous donnant ainsi le contrôle sur le comportement des objets et vous permettant d'implémenter des logiques de validation.

Les objectifs de ce laboratoire incluent la compréhension du protocole des descripteurs, la création et l'utilisation de descripteurs personnalisés, la mise en œuvre de la validation des données avec des descripteurs et l'optimisation des implémentations de descripteurs. Vous allez créer plusieurs fichiers au cours du laboratoire, notamment descrip.py, stock.py et validate.py.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ControlFlowGroup(["Control Flow"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python(("Python")) -.-> python/ErrorandExceptionHandlingGroup(["Error and Exception Handling"]) python/ControlFlowGroup -.-> python/conditional_statements("Conditional Statements") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/encapsulation("Encapsulation") python/ErrorandExceptionHandlingGroup -.-> python/raising_exceptions("Raising Exceptions") subgraph Lab Skills python/conditional_statements -.-> lab-132501{{"Apprenez-en sur les descripteurs"}} python/function_definition -.-> lab-132501{{"Apprenez-en sur les descripteurs"}} python/classes_objects -.-> lab-132501{{"Apprenez-en sur les descripteurs"}} python/encapsulation -.-> lab-132501{{"Apprenez-en sur les descripteurs"}} python/raising_exceptions -.-> lab-132501{{"Apprenez-en sur les descripteurs"}} end

Comprendre le protocole des descripteurs

Dans cette étape, nous allons apprendre comment les descripteurs (descriptors) fonctionnent en Python en créant une simple classe Stock. Les descripteurs en Python sont une fonctionnalité puissante qui vous permet de personnaliser la manière dont les attributs sont accédés, définis et supprimés. Le protocole des descripteurs se compose de trois méthodes spéciales : __get__(), __set__() et __delete__(). Ces méthodes définissent le comportement du descripteur lorsque l'attribut est accédé, reçoit une valeur ou est supprimé, respectivement.

Tout d'abord, nous devons créer un nouveau fichier appelé stock.py dans le répertoire du projet. Ce fichier contiendra notre classe Stock. Voici le code que vous devez placer dans le fichier stock.py :

## stock.py

class Stock:
    __slots__ = ['_name', '_shares', '_price']

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        self._name = value

    @property
    def shares(self):
        return self._shares

    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected an integer')
        if value < 0:
            raise ValueError('Expected a positive value')
        self._shares = value

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if not isinstance(value, (int, float)):
            raise TypeError('Expected a number')
        if value < 0:
            raise ValueError('Expected a positive value')
        self._price = value

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

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

    def __repr__(self):
        return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'

Dans cette classe Stock, nous utilisons le décorateur property pour définir les méthodes getter et setter pour les attributs name, shares et price. Ces méthodes getter et setter agissent comme des descripteurs, ce qui signifie qu'elles contrôlent la manière dont ces attributs sont accédés et définis. Par exemple, les méthodes setter valident les valeurs d'entrée pour s'assurer qu'elles sont du bon type et dans une plage acceptable.

Maintenant que notre fichier stock.py est prêt, ouvrons un interpréteur Python pour expérimenter avec la classe Stock et voir comment les descripteurs fonctionnent en pratique. Pour ce faire, ouvrez votre terminal et exécutez les commandes suivantes :

cd ~/project
python3 -i stock.py

L'option -i dans la commande python3 indique à Python de démarrer un interpréteur interactif après avoir exécuté le fichier stock.py. De cette façon, nous pouvons interagir directement avec la classe Stock que nous venons de définir.

Dans l'interpréteur Python, créons un objet Stock et essayons d'accéder à ses attributs. Voici comment vous pouvez le faire :

s = Stock('GOOG', 100, 490.10)
s.name      ## Should return 'GOOG'
s.shares    ## Should return 100

Lorsque vous accédez aux attributs name et shares de l'objet s, Python utilise en réalité la méthode __get__ du descripteur en arrière-plan. Les décorateurs property de notre classe sont implémentés à l'aide de descripteurs, ce qui signifie qu'ils gèrent l'accès et l'affectation des attributs de manière contrôlée.

Regardons de plus près le dictionnaire de classe pour voir les objets descripteurs. Le dictionnaire de classe contient tous les attributs et méthodes définis dans la classe. Vous pouvez afficher les clés du dictionnaire de classe en utilisant le code suivant :

Stock.__dict__.keys()

Vous devriez voir une sortie similaire à ceci :

dict_keys(['__module__', '__slots__', '__init__', 'name', '_name', 'shares', '_shares', 'price', '_price', 'cost', 'sell', '__repr__', '__doc__'])

Les clés name, shares et price représentent les objets descripteurs créés par les décorateurs property.

Maintenant, examinons comment les descripteurs fonctionnent en appelant manuellement leurs méthodes. Nous utiliserons le descripteur shares comme exemple. Voici comment vous pouvez le faire :

## Get the descriptor object for 'shares'
q = Stock.__dict__['shares']

## Use the __get__ method to retrieve the value
q.__get__(s, Stock)  ## Should return 100

## Use the __set__ method to set a new value
q.__set__(s, 75)
s.shares  ## Should now be 75

## Try to set an invalid value
try:
    q.__set__(s, '75')  ## Should raise TypeError
except TypeError as e:
    print(f"Error: {e}")

Lorsque vous accédez à un attribut comme s.shares, Python appelle la méthode __get__ du descripteur pour récupérer la valeur. Lorsque vous assignez une valeur comme s.shares = 75, Python appelle la méthode __set__ du descripteur. Le descripteur peut alors valider les données et lever des erreurs si la valeur d'entrée n'est pas valide.

Une fois que vous avez terminé d'expérimenter avec la classe Stock et les descripteurs, vous pouvez quitter l'interpréteur Python en exécutant la commande suivante :

exit()

Création de descripteurs personnalisés

Dans cette étape, nous allons créer notre propre classe de descripteur. Mais d'abord, comprenons ce qu'est un descripteur. Un descripteur est un objet Python qui implémente le protocole des descripteurs, qui se compose des méthodes __get__, __set__ et __delete__. Ces méthodes permettent au descripteur de gérer la manière dont un attribut est accédé, défini et supprimé. En créant notre propre classe de descripteur, nous pouvons mieux comprendre le fonctionnement de ce protocole.

Créez un nouveau fichier appelé descrip.py dans le répertoire du projet. Ce fichier contiendra notre classe de descripteur personnalisé. Voici le code :

## descrip.py

class Descriptor:
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, cls):
        print(f'{self.name}:__get__')
        ## In a real descriptor, you would return a value here

    def __set__(self, instance, value):
        print(f'{self.name}:__set__ {value}')
        ## In a real descriptor, you would store the value here

    def __delete__(self, instance):
        print(f'{self.name}:__delete__')
        ## In a real descriptor, you would delete the value here

Dans la classe Descriptor, la méthode __init__ initialise le descripteur avec un nom. La méthode __get__ est appelée lorsque l'attribut est accédé, la méthode __set__ est appelée lorsque l'attribut est défini, et la méthode __delete__ est appelée lorsque l'attribut est supprimé.

Maintenant, créons un fichier de test pour expérimenter avec notre descripteur personnalisé. Cela nous aidera à voir le comportement du descripteur dans différents scénarios. Créez un fichier nommé test_descrip.py avec le code suivant :

## test_descrip.py

from descrip import Descriptor

class Foo:
    a = Descriptor('a')
    b = Descriptor('b')
    c = Descriptor('c')

## Create an instance and try accessing the attributes
if __name__ == '__main__':
    f = Foo()
    print("Accessing attribute f.a:")
    f.a

    print("\nAccessing attribute f.b:")
    f.b

    print("\nSetting attribute f.a = 23:")
    f.a = 23

    print("\nDeleting attribute f.a:")
    del f.a

Dans le fichier test_descrip.py, nous importons la classe Descriptor depuis descrip.py. Ensuite, nous créons une classe Foo avec trois attributs a, b et c, chacun géré par un descripteur. Nous créons une instance de Foo et effectuons des opérations telles que l'accès, la définition et la suppression d'attributs pour voir comment les méthodes du descripteur sont appelées.

Maintenant, exécutons ce fichier de test pour voir les descripteurs en action. Ouvrez votre terminal, accédez au répertoire du projet et exécutez le fichier de test en utilisant les commandes suivantes :

cd ~/project
python3 test_descrip.py

Vous devriez voir une sortie comme celle-ci :

Accessing attribute f.a:
a:__get__

Accessing attribute f.b:
b:__get__

Setting attribute f.a = 23:
a:__set__ 23

Deleting attribute f.a:
a:__delete__

Comme vous pouvez le voir, chaque fois que vous accédez, définissez ou supprimez un attribut géré par un descripteur, la méthode magique correspondante (__get__, __set__ ou __delete__) est appelée.

Examinons également notre descripteur de manière interactive. Cela nous permettra de tester le descripteur en temps réel et de voir immédiatement les résultats. Ouvrez votre terminal, accédez au répertoire du projet et démarrez une session Python interactive avec le fichier descrip.py :

cd ~/project
python3 -i descrip.py

Maintenant, tapez ces commandes dans la session Python interactive pour voir comment le protocole des descripteurs fonctionne :

class Foo:
    a = Descriptor('a')
    b = Descriptor('b')
    c = Descriptor('c')

f = Foo()
f.a         ## Should call __get__
f.b         ## Should call __get__
f.a = 23    ## Should call __set__
del f.a     ## Should call __delete__
exit()

L'idée clé ici est que les descripteurs offrent un moyen d'intercepter et de personnaliser l'accès aux attributs. Cela les rend puissants pour implémenter la validation des données, les attributs calculés et d'autres comportements avancés. En utilisant des descripteurs, vous pouvez avoir plus de contrôle sur la manière dont les attributs de votre classe sont accédés, définis et supprimés.

✨ Vérifier la solution et pratiquer

Mise en œuvre de validateurs à l'aide de descripteurs

Dans cette étape, nous allons créer un système de validation à l'aide de descripteurs. Mais d'abord, comprenons ce que sont les descripteurs et pourquoi nous les utilisons. Les descripteurs sont des objets Python qui implémentent le protocole des descripteurs, qui inclut les méthodes __get__, __set__ ou __delete__. Ils vous permettent de personnaliser la manière dont un attribut est accédé, défini ou supprimé sur un objet. Dans notre cas, nous utiliserons les descripteurs pour créer un système de validation qui garantit l'intégrité des données. Cela signifie que les données stockées dans nos objets respecteront toujours certains critères, comme être d'un type spécifique ou avoir une valeur positive.

Maintenant, commençons à créer notre système de validation. Nous allons créer un nouveau fichier appelé validate.py dans le répertoire du projet. Ce fichier contiendra les classes qui implémentent nos validateurs.

## validate.py

class Validator:
    def __init__(self, name):
        self.name = name

    @classmethod
    def check(cls, value):
        return value

    def __set__(self, instance, value):
        instance.__dict__[self.name] = self.check(value)


class String(Validator):
    expected_type = str

    @classmethod
    def check(cls, value):
        if not isinstance(value, cls.expected_type):
            raise TypeError(f'Expected {cls.expected_type}')
        return value


class PositiveInteger(Validator):
    expected_type = int

    @classmethod
    def check(cls, value):
        if not isinstance(value, cls.expected_type):
            raise TypeError(f'Expected {cls.expected_type}')
        if value < 0:
            raise ValueError('Expected a positive value')
        return value


class PositiveFloat(Validator):
    expected_type = float

    @classmethod
    def check(cls, value):
        if not isinstance(value, (int, float)):
            raise TypeError('Expected a number')
        if value < 0:
            raise ValueError('Expected a positive value')
        return float(value)

Dans le fichier validate.py, nous définissons d'abord une classe de base appelée Validator. Cette classe a une méthode __init__ qui prend un paramètre name, qui sera utilisé pour identifier l'attribut en cours de validation. La méthode check est une méthode de classe qui renvoie simplement la valeur passée à elle. La méthode __set__ est une méthode de descripteur qui est appelée lorsqu'un attribut est défini sur un objet. Elle appelle la méthode check pour valider la valeur, puis stocke la valeur validée dans le dictionnaire de l'objet.

Nous définissons ensuite trois sous - classes de Validator : String, PositiveInteger et PositiveFloat. Chacune de ces sous - classes remplace la méthode check pour effectuer des vérifications de validation spécifiques. La classe String vérifie si la valeur est une chaîne de caractères, la classe PositiveInteger vérifie si la valeur est un entier positif, et la classe PositiveFloat vérifie si la valeur est un nombre positif (soit un entier, soit un flottant).

Maintenant que nous avons défini nos validateurs, modifions notre classe Stock pour utiliser ces validateurs. Nous allons créer un nouveau fichier appelé stock_with_validators.py et importer les validateurs depuis le fichier validate.py.

## stock_with_validators.py

from validate import String, PositiveInteger, PositiveFloat

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

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

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

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

    def __repr__(self):
        return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'

Dans le fichier stock_with_validators.py, nous définissons la classe Stock et utilisons les validateurs comme attributs de classe. Cela signifie que chaque fois qu'un attribut est défini sur un objet Stock, la méthode __set__ du validateur correspondant sera appelée pour valider la valeur. La méthode __init__ initialise les attributs de l'objet Stock, et les méthodes cost, sell et __repr__ fournissent des fonctionnalités supplémentaires.

Maintenant, testons notre classe Stock basée sur des validateurs. Nous allons ouvrir un terminal, accéder au répertoire du projet et exécuter le fichier stock_with_validators.py en mode interactif.

cd ~/project
python3 -i stock_with_validators.py

Une fois que l'interpréteur Python est en cours d'exécution, nous pouvons essayer quelques commandes pour tester le système de validation.

## Create a stock with valid values
s = Stock('GOOG', 100, 490.10)
print(s.name)    ## Should return 'GOOG'
print(s.shares)  ## Should return 100
print(s.price)   ## Should return 490.1

## Try changing to valid values
s.shares = 75
print(s.shares)  ## Should return 75

## Try setting invalid values
try:
    s.shares = '75'  ## Should raise TypeError
except TypeError as e:
    print(f"Error setting shares to string: {e}")

try:
    s.shares = -50  ## Should raise ValueError
except ValueError as e:
    print(f"Error setting negative shares: {e}")

exit()

Dans le code de test, nous créons d'abord un objet Stock avec des valeurs valides et affichons ses attributs pour vérifier qu'ils sont correctement définis. Nous essayons ensuite de changer l'attribut shares pour une valeur valide et l'affichons à nouveau pour confirmer le changement. Enfin, nous essayons de définir l'attribut shares sur une valeur invalide (une chaîne de caractères et un nombre négatif) et capturons les exceptions levées par les validateurs.

Remarquez à quel point notre code est maintenant beaucoup plus propre. La classe Stock n'a plus besoin d'implémenter toutes ces méthodes de propriétés - les validateurs gèrent toutes les vérifications de type et les contraintes.

Les descripteurs nous ont permis de créer un système de validation réutilisable qui peut être appliqué à n'importe quel attribut de classe. C'est un modèle puissant pour maintenir l'intégrité des données dans toute votre application.

✨ Vérifier la solution et pratiquer

Amélioration de l'implémentation des descripteurs

Dans cette étape, nous allons améliorer notre implémentation des descripteurs. Vous avez peut - être remarqué que dans certains cas, nous avons spécifié les noms de manière redondante. Cela peut rendre notre code un peu désordonné et plus difficile à maintenir. Pour résoudre ce problème, nous allons utiliser la méthode __set_name__, une fonctionnalité utile introduite en Python 3.6.

La méthode __set_name__ est appelée automatiquement lorsque la classe est définie. Son principal rôle est de définir le nom du descripteur pour nous, de sorte que nous n'ayons pas à le faire manuellement à chaque fois. Cela rendra notre code plus propre et plus efficace.

Maintenant, mettons à jour votre fichier validate.py pour inclure la méthode __set_name__. Voici à quoi ressemblera le code mis à jour :

## validate.py

class Validator:
    def __init__(self, name=None):
        self.name = name

    def __set_name__(self, cls, name):
        ## This gets called when the class is defined
        ## It automatically sets the name of the descriptor
        if self.name is None:
            self.name = name

    @classmethod
    def check(cls, value):
        return value

    def __set__(self, instance, value):
        instance.__dict__[self.name] = self.check(value)


class String(Validator):
    expected_type = str

    @classmethod
    def check(cls, value):
        if not isinstance(value, cls.expected_type):
            raise TypeError(f'Expected {cls.expected_type}')
        return value


class PositiveInteger(Validator):
    expected_type = int

    @classmethod
    def check(cls, value):
        if not isinstance(value, cls.expected_type):
            raise TypeError(f'Expected {cls.expected_type}')
        if value < 0:
            raise ValueError('Expected a positive value')
        return value


class PositiveFloat(Validator):
    expected_type = float

    @classmethod
    def check(cls, value):
        if not isinstance(value, (int, float)):
            raise TypeError('Expected a number')
        if value < 0:
            raise ValueError('Expected a positive value')
        return float(value)

Dans le code ci - dessus, la méthode __set_name__ de la classe Validator vérifie si l'attribut name est None. Si c'est le cas, elle définit name sur le nom de l'attribut réel utilisé dans la définition de la classe. De cette façon, nous n'avons pas besoin de spécifier le nom explicitement lors de la création d'instances des classes de descripteurs.

Maintenant que nous avons mis à jour le fichier validate.py, nous pouvons créer une version améliorée de notre classe Stock. Cette nouvelle version ne nous obligera pas à spécifier les noms de manière redondante. Voici le code de la classe Stock améliorée :

## improved_stock.py

from validate import String, PositiveInteger, PositiveFloat

class Stock:
    name = String()  ## No need to specify 'name' anymore
    shares = PositiveInteger()
    price = PositiveFloat()

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

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

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

    def __repr__(self):
        return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'

Dans cette classe Stock, nous créons simplement des instances des classes de descripteurs String, PositiveInteger et PositiveFloat sans spécifier les noms. La méthode __set_name__ de la classe Validator s'occupera de définir les noms automatiquement.

Testons notre classe Stock améliorée. Tout d'abord, ouvrez votre terminal et accédez au répertoire du projet. Ensuite, exécutez le fichier improved_stock.py en mode interactif. Voici les commandes pour cela :

cd ~/project
python3 -i improved_stock.py

Une fois que vous êtes dans la session Python interactive, vous pouvez essayer les commandes suivantes pour tester la fonctionnalité de la classe Stock :

s = Stock('GOOG', 100, 490.10)
print(s.name)    ## Should return 'GOOG'
print(s.shares)  ## Should return 100
print(s.price)   ## Should return 490.1

## Try changing values
s.shares = 75
print(s.shares)  ## Should return 75

## Try invalid values
try:
    s.shares = '75'  ## Should raise TypeError
except TypeError as e:
    print(f"Error: {e}")

try:
    s.price = -10.5  ## Should raise ValueError
except ValueError as e:
    print(f"Error: {e}")

exit()

Ces commandes créent une instance de la classe Stock, affichent ses attributs, modifient la valeur d'un attribut, puis essaient de définir des valeurs invalides pour voir si les erreurs appropriées sont levées.

La méthode __set_name__ définit automatiquement le nom du descripteur lorsque la classe est définie. Cela rend votre code plus propre et moins redondant, car vous n'avez plus besoin de spécifier le nom de l'attribut deux fois.

Cette amélioration démontre comment le protocole des descripteurs de Python continue d'évoluer, rendant plus facile l'écriture de code propre et maintenable.

✨ Vérifier la solution et pratiquer

Résumé

Dans ce laboratoire (lab), vous avez appris à connaître les descripteurs Python, une fonctionnalité puissante permettant de personnaliser l'accès aux attributs dans les classes. Vous avez exploré le protocole des descripteurs, y compris les méthodes __get__, __set__ et __delete__. Vous avez également créé une classe de descripteur de base pour intercepter l'accès aux attributs et utilisé des descripteurs pour implémenter un système de validation pour garantir l'intégrité des données.

De plus, vous avez amélioré vos descripteurs avec la méthode __set_name__ pour réduire la redondance. Les descripteurs sont largement utilisés dans les bibliothèques et les frameworks Python tels que Django et SQLAlchemy. Comprendre les descripteurs vous permet d'approfondir vos connaissances de Python et vous aide à écrire un code plus élégant et plus facilement maintenable.