Qu'est-ce que la méthode __eq__ ?
La méthode spéciale __eq__ (eq pour equal) permet de définir le comportement de l'opérateur == sur les objets des classes que vous allez créer. Imaginez une instance a et une instance b. Python appelle en réalité a.__eq__(b). Si vous ne définissez pas cette méthode, par défaut Python comparera l'identité des objets. C'est-à-dire, si les deux objets ont le même emplacement en mémoire, comme ce que vous pouvez faire avec l'opérateur is.
class Utilisateur: def __init__(self, nom, email): self.nom = nom self.email = email u1 = Utilisateur("Patrick", "patrick@exemple.fr") u2 = Utilisateur("Patrick", "patrick@exemple.fr") print(u1 == u2) # False print(u1 == u1) # True
Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.
Les deux objets ont les mêmes attributs, mais pour Python il s'agit de deux objets distincts. Pour une classe métier, vous allez bien souvent vouloir le modifier pour que u1 == u2 renvoie True dans ce cas.
Comment implémenter __eq__ ?
Il suffit d'implémenter la méthode et de comparer les attributs qui détermineront l'égalité :
class Utilisateur: def __init__(self, nom, email): self.nom = nom self.email = email def __eq__(self, autre): if not isinstance(autre, Utilisateur): return NotImplemented return self.nom == autre.nom and self.email == autre.email u1 = Utilisateur("Patrick", "patrick@exemple.fr") u2 = Utilisateur("Patrick", "patrick@exemple.fr") u3 = Utilisateur("Sebastien", "sebastien@exemple.fr") print(u1 == u2) # True print(u1 == u3) # False
Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.
Ici, l'égalité se fera sur le nom et l'adresse email. Aussi, vous verrez souvent NotImplemented et non False en valeur retour lorsque l'objet comparé n'est pas de la même classe. D'ailleurs, nous en parlons dans ce mentorat.
NotImplemented permet de dire à Python que notre classe ne sait pas faire cette comparaison. Python va alors essayer de faire la comparaison dans l'autre sens autre.__eq__(self), et si l'autre objet ne sait pas répondre, la comparaison se fera sur l'identité.
Pourquoi mon objet devient-il non hashable ?
Dès que __eq__ est définie dans une classe, Python met automatiquement __hash__ à None. Vous ne pouvez donc plus utiliser votre objet dans un ensemble ou un dictionnaire.
Au passage, n'hésitez pas à aller voir notre notre glossaire sur __hash__.
class Utilisateur: def __init__(self, nom, email): self.nom = nom self.email = email def __eq__(self, autre): if not isinstance(autre, Utilisateur): return NotImplemented return self.nom == autre.nom and self.email == autre.email u1 = Utilisateur("Patrick", "patrick@exemple.fr") u2 = Utilisateur("Patrick", "patrick@exemple.fr") u3 = Utilisateur("Sebastien", "sebastien@exemple.fr") employes = {u1, u2, u3} # TypeError: cannot use 'Utilisateur' as a set element (unhashable type: 'Utilisateur')
Tout comme les dictionnaires, un ensemble (set) ne parcourt pas tous ses éléments un par un pour retrouver un objet. Il calcule le hash de l'objet et s'en sert pour le "ranger". Pour vérifier la présence d'un objet, il recalcule le hash et va directement retrouver l'objet.
Ce qui implique que deux objets égaux doivent avoir le même hash. Comme le __hash__ par défaut est calculé à partir de l'identité (l'emplacement en mémoire), les deux Patrick, égaux grâce à __eq__, auraient deux hash différents. L'ensemble prendrait les deux objets, on aurait donc un set avec des doublons, c'est impossible ! C'est pour éviter ce genre de cas que Python désactive le hachage dès que vous définissez __eq__.
Comment implémenter __hash__ ?
Ce que vous verrez le plus souvent : passer un tuple avec les attributs utilisés dans __eq__ à la fonction hash() :
class Utilisateur: def __init__(self, nom, email): self.nom = nom self.email = email def __eq__(self, autre): if not isinstance(autre, Utilisateur): return NotImplemented return self.nom == autre.nom and self.email == autre.email def __hash__(self): return hash((self.nom, self.email)) u1 = Utilisateur("Patrick", "patrick@exemple.fr") u2 = Utilisateur("Patrick", "patrick@exemple.fr") utilisateurs = {u1, u2} print(len(utilisateurs)) # 1
Mêmes attributs, même hash, le set élimine bien le doublon.
Attention
Les attributs utilisés dans le hash ne doivent jamais changer.
class Utilisateur: def __init__(self, nom, email): self.nom = nom self.email = email def __eq__(self, autre): if not isinstance(autre, Utilisateur): return NotImplemented return self.nom == autre.nom and self.email == autre.email def __hash__(self): return hash((self.nom, self.email)) u1 = Utilisateur("Sebastien", "sebastien@exemple.fr") utilisateurs = {u1} print(u1 in utilisateurs) # True u1.email = "direction@exemple.fr" print(u1 in utilisateurs) # False print(len(utilisateurs)) # 1
Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.
L'objet est toujours dans le set, qui a bien une longueur de 1, mais il est introuvable ! Il est rangé dans le set avec la valeur de l'ancien hash, mais Python le cherche par son nouveau hash. En écrivant notre propre __hash__, nous sommes passés au-delà du garde-fou de Python et il nous fait confiance pour respecter le contrat.
Vous comprenez pourquoi les types muables ne sont pas hashables...
À noter
Avec les dataclasses, la méthode __eq__ est générée automatiquement et votre classe est non hashable. Cependant, si vous passez frozen=True au décorateur, la classe devient immuable, la méthode __hash__ est générée.
from dataclasses import dataclass @dataclass(frozen=True) class Utilisateur: nom: str email: str u1 = Utilisateur("Patrick", "patrick@exemple.fr") u2 = Utilisateur("Patrick", "patrick@exemple.fr") print(u1 == u2) # True print(len({u1, u2})) # 1
Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.