Faire se comporter vos objets comme des types prédéfinis
Affichez un objet ordinaire et Python montre quelque chose comme <__main__.Position object at 0x7f3a>, et Position("A", 1) == Position("A", 1) vaut False car, par défaut, deux objets ne sont égaux que s'il s'agit du même objet. Les méthodes spéciales (dites « dunder », pour les doubles tirets bas) permettent à votre classe de s'intégrer à la syntaxe de Python : __repr__ contrôle l'affichage d'un objet, __eq__ contrôle ==, __add__ contrôle +, __lt__ contrôle <, etc. On ne les appelle jamais directement : Python les appelle quand vous utilisez repr(x), a == b, a + b ou sorted(items).
class Plain:
def __init__(self, cents):
self.cents = cents
a, b = Plain(500), Plain(500)
print(a == b) # False: different objects
print(a.cents == b.cents) # True
print("object at 0x" in str(a)) # True: the default display is unhelpful__repr__ : montrer clairement l'objet
__repr__ renvoie une chaîne qui décrit l'objet pour les développeurs. Par convention, elle ressemble au code qui le construirait : Money(1999, 'USD'). Les listes, le débogueur et les messages d'erreur l'utilisent tous, ce qui rend les bogues bien plus lisibles. print(x) et str(x) utilisent aussi __repr__ sauf si vous définissez en plus __str__ pour un texte plus joli destiné à l'utilisateur. Elle doit renvoyer la chaîne, pas l'afficher, et {self.currency!r} dans une f-string donne la forme entre guillemets d'un attribut de type chaîne.
class Money:
def __init__(self, cents, currency="USD"):
self.cents = cents
self.currency = currency
def __repr__(self):
return f"Money({self.cents}, {self.currency!r})"
price = Money(1999)
print(price) # Money(1999, 'USD')
print([Money(5), Money(7, "EUR")]) # [Money(5, 'USD'), Money(7, 'EUR')]__eq__ et __hash__ : que veut dire égal ?
__eq__(self, other) décide de self == other. Comparez les champs qui définissent l'identité et, quand other n'est pas du type attendu, renvoyez la valeur spéciale NotImplemented (pas False) : Python essaie alors l'autre côté et répond finalement False lui-même, de sorte que Money(1) == 5 est sans danger. Définir __eq__ désactive le hachage : les objets ne peuvent plus être membres d'un ensemble ni clés de dictionnaire à moins de définir aussi __hash__ de façon cohérente, par exemple hash((self.cents, self.currency)).
class Money:
def __init__(self, cents, currency="USD"):
self.cents = cents
self.currency = currency
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented # let Python decide
return self.cents == other.cents and self.currency == other.currency
def __hash__(self):
return hash((self.cents, self.currency))
print(Money(500) == Money(500)) # True
print(Money(500) == Money(500, "EUR")) # False
print(Money(500) == 500) # False, no error
print(len({Money(1), Money(1)})) # 1: usable in sets nowOpérateurs et ordre
__add__(self, other) donne + à votre classe. Renvoyez un nouvel objet plutôt que de modifier self, et refusez l'absurde : additionner des dollars et des euros doit lever ValueError, additionner un simple nombre doit renvoyer NotImplemented. __lt__(self, other) donne <, et cela suffit à sorted(), min() et max() pour ordonner vos objets, car ils ont seulement besoin de demander « a est-il plus petit que b ? ». Ajoutez aussi __le__, __gt__, __ge__, ou laissez functools.total_ordering les déduire.
class Money:
def __init__(self, cents, currency="USD"):
self.cents = cents
self.currency = currency
def __repr__(self):
return f"Money({self.cents}, {self.currency!r})"
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented
if other.currency != self.currency:
raise ValueError("cannot add different currencies")
return Money(self.cents + other.cents, self.currency)
def __lt__(self, other):
if not isinstance(other, Money):
return NotImplemented
return self.cents < other.cents
total = Money(150) + Money(275)
print(total) # Money(425, 'USD')
print(sorted([Money(30), Money(10), Money(20)])) # [Money(10, 'USD'), Money(20, 'USD'), Money(30, 'USD')]
print(sum([Money(1), Money(2)], Money(0))) # Money(3, 'USD')__repr__ doit montrer tout ce que regarde __eq__. Ne donnez pas à + d'effets de bord surprenants : il doit renvoyer une nouvelle valeur et laisser les deux opérandes intacts, comme les nombres. Les tests du lab vérifient ces points : originaux inchangés, devises différentes refusées, types sans rapport donnant NotImplemented.__repr__renvoie une chaîne (commeMoney(1999, 'USD')) ; elle n'affiche rien.__eq__compare les champs et renvoieNotImplementedpour les types étrangers ; ajoutez__hash__pour garder le hachage.__add__renvoie un nouvel objet ;__lt__suffit poursorted,minetmax.
À suivre : les dataclasses, qui écrivent __init__, __repr__ et __eq__ à votre place.
Lab : ce qui est noté
- Afficher clairement un objet Money avec
__repr__25 pts - Dire quand deux montants sont égaux avec
__eq__25 pts - Additionner deux montants avec
__add__25 pts - Rendre les montants triables avec
__lt__25 pts