Accéder au contenu principal

Tutoriel sur les descripteurs Python

Découvrez ce que sont les descripteurs Python, quand les utiliser et pourquoi les utiliser.
Actualisé 19 sept. 2026  · 13 min lire

Explorer avec l’IA

ChatGPTClaudePerplexity

Les descripteurs Python, ou plus généralement les descripteurs, offrent une technique puissante pour écrire du code réutilisable et partageable entre plusieurs classes. Ils peuvent rappeler l’héritage, mais techniquement, ce n’en est pas. Il s’agit d’un mécanisme générique pour intercepter l’accès aux attributs. Les descripteurs sont le mécanisme sous-jacent des propriétés, des méthodes statiques, des méthodes de classe, de super, etc.

Les descripteurs ont été introduits avec Python 2.2 et, depuis, sont souvent considérés comme une « magie » ayant donné un nouveau style aux classes traditionnelles. Ce sont des classes qui permettent de gérer des propriétés au sein d’une autre classe. Concrètement, elles implémentent les méthodes d’interface __get__(), __set__() et __delete__(), ce qui les rend intéressantes pour de nombreuses raisons. Par exemple, les décorateurs de classes et de propriétés que vous avez sans doute déjà vus en Python.

Pour faire simple, une classe qui implémente __get__(), __set()__ ou __delete()__ pour un objet est appelée un « descripteur ». Pour citer directement la documentation officielle de Python, un descripteur est un attribut d’objet doté d’un comportement de liaison, dont l’accès a été redéfini par les méthodes du protocole de descripteur. Ces méthodes sont __get__(), __set__() et __delete__() (Source).

Le comportement de liaison appliqué aux descripteurs signifie que l’on contrôle la façon dont une valeur peut être affectée, lue (get) ou supprimée pour une variable, un objet ou un jeu de données donné. Toute l’interaction est ainsi liée à cette donnée précise : elle ne s’applique qu’aux données sur lesquelles vous l’avez définie, ce qui la lie à cette partie spécifique de l’état.

On distingue les descripteurs de données et les descripteurs non liés aux données. Si votre descripteur n’implémente que __get__(), c’est un descripteur non lié aux données ; à l’inverse, une implémentation comprenant __set__() et __delete__() est un descripteur de données. Les descripteurs non liés aux données sont uniquement en lecture, tandis que les descripteurs de données sont en lecture et en écriture.

Notez bien que les descripteurs sont attachés à une classe, et non aux instances. Modifier la classe remplace ou supprime le descripteur lui‑même, au lieu de déclencher son code (IBM Developer).

Enfin, une classe de descripteur n’est pas limitée à ces trois méthodes : elle peut contenir d’autres attributs et méthodes en plus de get, set et delete.

Voyons plus en détail les méthodes get, set et delete, en nous inspirant de cette page IBM Developer :

  • self est l’instance du descripteur que vous créez (Real Python).
  • object est l’instance de l’objet auquel votre descripteur est rattaché (Real Python).
  • type est le type de l’objet auquel le descripteur est rattaché (Real Python).
  • value est la valeur affectée à l’attribut du descripteur. get(self, object, type) set(self, object, value) delete(self, object)
  • __get__() est invoquée lors de l’accès à l’attribut ou lorsque vous souhaitez extraire une information. Elle renvoie la valeur de l’attribut ou lève une exception AttributeError si l’attribut demandé n’existe pas.

  • __set__() est appelée lors d’une affectation à l’attribut et définit sa valeur. Elle ne renvoie rien, mais peut lever une exception AttributeError.

  • __delete__() contrôle l’opération de suppression, c’est‑à‑dire lorsque vous supprimez l’attribut d’un objet. Elle ne renvoie rien.

Passons maintenant à des exemples pour comprendre à quoi servent les descripteurs.

Objectif des descripteurs

Définissons une classe car avec trois attributs : make, model et fuel_cap. Vous utiliserez la méthode __init__() pour initialiser les attributs. Puis vous utiliserez la méthode magique __str__(), qui renverra simplement une chaîne résumant les trois attributs passés à la classe lors de la création de l’objet.

Notez que la méthode __str__() renvoie la représentation textuelle de l’objet. Elle est appelée lorsque l’on invoque print() ou str() sur un objet de la classe.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car1 = Car("BMW","X7",40)
print(car1)
BMW model X7 with a fuel capacity of 40 ltr.

Comme vous le voyez dans la sortie ci‑dessus, tout semble parfait.

Changeons maintenant la capacité du réservoir à moins 40.

car2 = Car("BMW","X7",-40)
print(car2)
BMW model X7 with a fuel capacity of -40 ltr.

Minute… quelque chose cloche, non ? La capacité d’un réservoir ne peut pas être négative. Pourtant, Python accepte l’entrée sans erreur. C’est parce que Python est un langage dynamique qui ne fait pas de vérification de type explicite par défaut.

Pour éviter ce problème, ajoutons une condition if dans __init__() pour vérifier si la capacité saisie est valide. Si elle est invalide, levons une exception ValueError.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car1 = Car("BMW","X7",40)
print(car1)
BMW model X7 with a fuel capacity of 40 ltr.
car2 = Car("BMW","X7",-40)
print(car2)
----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-22-1c3d23be72f7> in <module>
----> 1 car2 = Car("BMW","X7",-40)
      2 print(car2)


<ipython-input-20-1e154783588d> in __init__(self, make, model, fuel_cap)
      5         self.fuel_cap = fuel_cap
      6         if self.fuel_cap < 0:
----> 7             raise ValueError("Fuel Capacity can never be less than zero")
      8
      9     def __str__(self):


ValueError: Fuel Capacity can never be less than zero

Comme on le voit, tout fonctionne pour l’instant : le programme lève une ValueError si la capacité est négative.

Mais un autre problème peut survenir : et si la fuel capacity était un flottant ou une chaîne ? Et si make ou model étaient des entiers ? Dans tous ces cas, le programme ne lèverait pas l’exception attendue.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car(-40,"X7",40)
print(car2)
-40 model X7 with a fuel capacity of 40 ltr.

Pour gérer ce cas, vous pourriez ajouter une autre condition if ou utiliser isinstance pour le contrôle de type.

Utilisons cette fois la fonction native isinstance.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if isinstance(self.make, str):
            print(self.make)
        else:
            raise ValueError("Make of the car can never be an integer")

        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
BMW
BMW model X7 with a fuel capacity of 40 ltr.
car2 = Car(-40,"X7",40)
print(car2)
----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-34-75b08cba454f> in <module>
----> 1 car2 = Car(-40,"X7",40)
      2 print(car2)


<ipython-input-31-175690bf3b98> in __init__(self, make, model, fuel_cap)
      7             print(self.make)
      8         else:
----> 9             raise ValueError("Make of the car can never be an integer")
     10
     11         if self.fuel_cap < 0:


ValueError: Make of the car can never be an integer

Parfait, cette erreur est elle aussi correctement gérée.

Mais si vous modifiez plus tard l’attribut fuel capacity pour le passer à moins 40, cela ne fonctionnera pas : la vérification de type n’a lieu qu’une seule fois dans __init__(). Comme vous le savez, __init__() est un constructeur appelé uniquement lors de la création de l’objet. La vérification personnalisée échouera donc ensuite.

Illustrons‑le.

class Car:
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap
        if isinstance(self.make, str):
            print(self.make)
        else:
            raise ValueError("Make of the car can never be an integer")

        if self.fuel_cap < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
BMW
BMW model X7 with a fuel capacity of 40 ltr.
car2.make = -40
print(car2)
-40 model X7 with a fuel capacity of 40 ltr.

Et voilà, vous avez contourné la vérification de type.

Imaginez maintenant que vous ayez bien d’autres attributs (consommation, prix, options, etc.) nécessitant aussi un contrôle de type, avec en plus des règles d’accès en lecture seule pour certains d’entre eux. Fastidieux, n’est‑ce pas ?

Heureusement, les descripteurs Python permettent de traiter tout cela efficacement.

Comme vous l’avez vu, toute classe qui implémente les méthodes magiques __get__(), __set()__ ou __delete()__ du protocole de descripteur est un descripteur. Ils vous donnent aussi un contrôle fin sur le fonctionnement d’un attribut : accès en lecture seule, lecture/écriture, etc.

Prolongeons maintenant l’exemple ci‑dessus en ajoutant les méthodes de descripteur Python.

class Descriptor:
    def __init__(self):
        self.__fuel_cap = 0
    def __get__(self, instance, owner):    
        return self.__fuel_cap
    def __set__(self, instance, value):
        if isinstance(value, int):
            print(value)
        else:
            raise TypeError("Fuel Capacity can only be an integer")

        if value < 0:
            raise ValueError("Fuel Capacity can never be less than zero")

        self.__fuel_cap = value

    def __delete__(self, instance):
        del self.__fuel_cap
class Car:
    fuel_cap = Descriptor()
    def __init__(self,make,model,fuel_cap):
        self.make = make
        self.model = model
        self.fuel_cap = fuel_cap

    def __str__(self):
        return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
40
BMW model X7 with a fuel capacity of 40 ltr.

Pas d’inquiétude si la classe Descriptor vous paraît obscure : découpons‑la.

  • La méthode __init__() de Descriptor initialise la variable locale __fuel_cap à zéro. Le double underscore (dunder) en début de nom la rend privée. Ici, il sert aussi à distinguer l’attribut de capacité du descripteur de celui de la classe Car.
  • Comme vous le savez désormais, __get__() sert à récupérer l’attribut et renvoie la capacité. Elle prend trois arguments : l’objet descripteur, l’instance de la classe qui contient ce descripteur (ici car2) et enfin le propriétaire, c’est‑à‑dire la classe dont l’instance dépend, ici Car. Dans cette méthode, vous retournez simplement la valeur de fuel_cap, définie dans __set__().
  • __set__() est appelée lors de l’affectation d’une valeur à l’attribut et, contrairement à __get__(), ne renvoie rien. Outre l’objet descripteur lui‑même, elle reçoit l’instance (comme dans __get__()) et la valeur à affecter. Ici, vous vérifiez que la valeur assignée à fuel_cap est bien un entier ; sinon, vous levez une TypeError. Vous vérifiez ensuite qu’elle n’est pas négative ; le cas échéant, vous levez une ValueError. Après ces contrôles, vous mettez à jour fuel_cap avec la valeur.
  • Enfin, __delete__() est appelée lors de la suppression de l’attribut de l’objet et, comme __set__(), ne renvoie rien.

La classe Car reste inchangée, à ceci près que vous y ajoutez l’instance fuel_cap de la classe Descriptor(). Comme indiqué plus haut, l’instance de descripteur doit être ajoutée en tant qu’attribut de classe, et non d’instance.

Dès que vous affectez la variable locale fuel_cap dans __init__() à l’instance fuel_cap, cela invoque la méthode __set__() du descripteur.

Modifions maintenant la capacité du réservoir avec une valeur négative pour vérifier que l’exception ValueError est bien levée.

car2 = Car("BMW","X7",-40)
print(car2)
-40



----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-115-1c3d23be72f7> in <module>
----> 1 car2 = Car("BMW","X7",-40)
      2 print(car2)


<ipython-input-107-3e1f3e97d3c7> in __init__(self, make, model, fuel_cap)
      4         self.make = make
      5         self.model = model
----> 6         self.fuel_cap = fuel_cap
      7
      8     def __str__(self):


<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
     11
     12         if value < 0:
---> 13             raise ValueError("Fuel Capacity can never be less than zero")
     14
     15         self.__fuel_cap = value


ValueError: Fuel Capacity can never be less than zero

Si vous vous souvenez ici, la vérification échouait lorsque vous modifiiez l’attribut après coup, car elle n’avait lieu qu’une fois dans __init__(). Essayons maintenant d’affecter une chaîne pour voir si une erreur se produit.

car2.fuel_cap = -1
-1



----------------------------------------

ValueErrorTraceback (most recent call last)

<ipython-input-120-dea9dbe96ebe> in <module>
----> 1 car2.fuel_cap = -1


<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
     11
     12         if value < 0:
---> 13             raise ValueError("Fuel Capacity can never be less than zero")
     14
     15         self.__fuel_cap = value


ValueError: Fuel Capacity can never be less than zero
car2.fuel_cap = "BMW"
----------------------------------------

TypeErrorTraceback (most recent call last)

<ipython-input-121-0b316a9872c6> in <module>
----> 1 car2.fuel_cap = "BMW"


<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
      8             print(value)
      9         else:
---> 10             raise TypeError("Fuel Capacity can only be an integer")
     11
     12         if value < 0:


TypeError: Fuel Capacity can only be an integer

Impeccable ! Comme vous le voyez, cela fonctionne aussi lors d’une mise à jour ultérieure de l’attribut.

Il subsiste toutefois un léger écueil : lorsque vous créez une nouvelle instance de la classe, la valeur précédente est écrasée. La raison est que les descripteurs sont liés à la classe et non à l’instance.

Voyons cela avec l’exemple ci‑dessous.

car3 = Car("BMW","X7",48) #created a new instance 'car3' with different values
48

Si vous affichez l’instance car2, vous constaterez que ses valeurs ont été écrasées par celles de car3.

print(car2)
BMW model X7 with a fuel capacity of 48 ltr.

Conclusion

Félicitations pour avoir terminé ce tutoriel.

Ce tutoriel s’adresse aux personnes déjà à l’aise avec Python et souhaitant monter en puissance sur des concepts avancés. Comme bon exercice, vous pouvez chercher comment résoudre le problème d’écrasement entre instances évoqué aujourd’hui.

N’hésitez pas à poser vos questions dans les commentaires ci‑dessous.

Pour aller plus loin en Python, suivez le parcours de compétences complet Python Programming de DataCamp.

Découvrez aussi notre tutoriel sur les fonctions Python.

Sujets
Python

Cours Python

Cours

Introduction à Python

4 h
7M
Apprenez les bases de l’analyse de données avec Python en quatre heures et explorez ses principaux packages.
Afficher les détailsRight Arrow
Commencer Le Cours
Voir plusRight Arrow