backend / python
POO : méthodes spéciales (dunder) et propriétés
Explication
Ce que vous allez apprendre
- Comprendre le lien entre
len(x),x + y,x == yet les méthodes dunder qu'ils appellent - Implémenter
__eq__et__hash__ensemble, sans jamais oublier l'un des deux - Rendre une classe itérable, indexable ou "appelable" via
__iter__,__getitem__,__call__ - Utiliser
@propertypour exposer une méthode comme un attribut, avec validation à l'écriture - Distinguer le rôle de
__str__(affichage humain) de celui de__repr__(débogage)
Dans quel contexte ?
Un développeur modélise des coordonnées géographiques avec une classe Vecteur2D et veut pouvoir écrire distance_totale = v1 + v2 + v3 comme il le ferait avec des nombres, plutôt que v1.additionner(v2).additionner(v3). Implémenter __add__ permet exactement cela : Python appelle automatiquement cette méthode dès qu'il rencontre + entre deux instances de la classe, rendant le code aussi lisible qu'avec des types natifs.
Faire parler le langage de vos propres objets
Quand vous écrivez len(ma_liste) ou a + b, Python ne fait pas de magie : il appelle en coulisses une méthode spéciale de l'objet, respectivement __len__ et __add__. Ces méthodes, encadrées de doubles underscores (d'où leur surnom dunder, pour double underscore), forment un protocole que n'importe quelle classe personnalisée peut implémenter pour se comporter comme un type natif de Python.
Pourquoi ça vaut la peine de les définir
Sans ces méthodes, additionner deux objets Vecteur2D obligerait à écrire une méthode .additionner() explicite, moins naturelle à lire qu'un simple v1 + v2. En implémentant __add__, __eq__ ou __iter__, on rend ses propres classes aussi agréables à utiliser que les types intégrés du langage, tout en gardant un contrôle total sur ce que signifie réellement additionner deux objets de son domaine métier.
Piège fréquent
Définir __eq__ sans définir __hash__ rend l'objet non hashable : impossible de le mettre dans un set ou de l'utiliser comme clé de dict, avec une erreur TypeError: unhashable type qui peut surprendre bien après l'écriture de la classe.
Un couple à ne jamais séparer
Si une classe définit __eq__ (pour comparer deux instances avec ==), elle doit aussi définir __hash__, sinon Python considère l'objet comme non hashable et refuse de l'utiliser dans un set ou comme clé de dictionnaire. C'est une règle de cohérence : deux objets égaux doivent produire le même hash.
Un attribut qui exécute du code
Une propriété permet d'exposer une méthode comme si c'était un simple attribut, tout en gardant la possibilité de valider une valeur à l'écriture (@x.setter) ou de la calculer à la volée à la lecture, comme la conversion Celsius/Fahrenheit de cette leçon. C'est l'outil idéal pour ajouter de la validation sans casser le code existant qui accédait déjà à l'attribut directement.
| Dunder | Déclenché par | Nécessite aussi |
|---|---|---|
__eq__ | a == b | __hash__ si l'objet doit rester hashable |
__add__ | a + b | __radd__ pour gérer nombre + a |
__iter__ | for x in a, unpacking | __next__ si l'objet est son propre itérateur |
__getitem__ | a[i] | Rien d'obligatoire |
Astuce
Avant d'écrire un setter avec if valeur < 0: raise ValueError(...), vérifiez si @property est réellement nécessaire : si l'attribut n'a besoin d'aucune validation ni calcul, un simple attribut public suffit et évite du code superflu.
Commandes & code
Méthodes spéciales (dunder) et propriétés
Intégrer ses objets aux opérateurs et syntaxes natives de Python.
class Vecteur2D:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __repr__(self):
return f"Vecteur2D({self.x}, {self.y})"
def __eq__(self, autre):
if not isinstance(autre, Vecteur2D):
return NotImplemented
return self.x == autre.x and self.y == autre.y
def __hash__(self):
# necessaire si on veut mettre l'objet dans un set/dict et qu'on a defini __eq__
return hash((self.x, self.y))
def __add__(self, autre):
return Vecteur2D(self.x + autre.x, self.y + autre.y)
def __sub__(self, autre):
return Vecteur2D(self.x - autre.x, self.y - autre.y)
def __mul__(self, scalaire):
return Vecteur2D(self.x * scalaire, self.y * scalaire)
def __rmul__(self, scalaire):
# permet aussi "2 * vecteur" (l'entier est a gauche, ne sait pas multiplier un vecteur)
return self.__mul__(scalaire)
def __neg__(self):
return Vecteur2D(-self.x, -self.y)
def __abs__(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
def __len__(self):
return 2
def __getitem__(self, index):
return (self.x, self.y)[index]
def __iter__(self):
yield self.x
yield self.y
def __bool__(self):
return abs(self) > 0
v1 = Vecteur2D(1, 2)
v2 = Vecteur2D(3, 4)
print(v1 + v2) # Vecteur2D(4, 6)
print(v1 - v2) # Vecteur2D(-2, -2)
print(v1 * 3) # Vecteur2D(3, 6)
print(2 * v1) # Vecteur2D(2, 4) via __rmul__
print(abs(v2)) # 5.0
print(len(v1)) # 2
print(v1[0], v1[1]) # 1 2
x, y = v1 # deballage grace a __iter__
print(list(v1)) # [1, 2]
print(bool(Vecteur2D(0, 0))) # False via __bool__
# --- Properties : accesseurs Pythoniques ---
class Temperature:
def __init__(self, celsius: float = 0):
self._celsius = celsius
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, valeur: float) -> None:
if valeur < -273.15:
raise ValueError("Temperature en dessous du zero absolu")
self._celsius = valeur
@property
def fahrenheit(self) -> float:
# propriete en lecture seule, calculee a la volee
return self._celsius * 9 / 5 + 32
@fahrenheit.setter
def fahrenheit(self, valeur: float) -> None:
self.celsius = (valeur - 32) * 5 / 9
t = Temperature(25)
print(t.celsius, t.fahrenheit) # 25 77.0
t.fahrenheit = 98.6
print(round(t.celsius, 1)) # 37.0
try:
t.celsius = -300
except ValueError as e:
print(f"Refuse : {e}")
# __call__ : rendre une instance "appelable" comme une fonction
class Multiplicateur:
def __init__(self, facteur):
self.facteur = facteur
def __call__(self, x):
return x * self.facteur
doubler = Multiplicateur(2)
print(doubler(21)) # 42 -- l'instance se comporte comme une fonction
# __enter__/__exit__ : voir la lecon sur les context managers pour le detail| Dunder | Déclenché par |
|---|---|
__init__ | Classe(...) |
__repr__ / __str__ | repr(x) / str(x), print(x) |
__eq__ / __hash__ | ==, utilisation dans set/dict |
__add__ / __radd__ | x + y |
__len__ / __getitem__ | len(x), x[i] |
__iter__ | for ... in x, unpacking |
__call__ | x(...) |
Résumé
- Les méthodes dunder permettent d'intégrer un objet aux opérateurs natifs (
+,==,len,[]...). - Si
__eq__est défini,__hash__doit l'être aussi pour rester utilisable dans unset/dict. @propertyexpose une méthode comme un attribut, avec validation possible ausetter.
Exercices pratiques
Mission : rendre une classe Argent utilisable comme un nombre
Objectif : Implémenter le protocole dunder d'une classe de montants monétaires pour l'intégrer aux opérateurs natifs, tout en respectant le couple __eq__/__hash__.
Contexte
Une classe Argent représente un montant avec une devise (Argent(100, "EUR")). Un développeur veut pouvoir écrire total = argent1 + argent2 et mettre des instances d'Argent dans un set pour dédupliquer des transactions identiques. Il a défini __eq__ mais son code plante avec TypeError: unhashable type: 'Argent' dès qu'il tente d'utiliser un set.
Tu dois compléter le protocole dunder de cette classe, comprendre pourquoi l'erreur survient, et exposer un montant en centimes via une propriété calculée.