__eq__

Qu'est-ce que la méthode spéciale __eq__ en Python ?

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
PYTHON
Un instant

Créez un compte pour exécuter ce code

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
PYTHON
Un instant

Créez un compte pour exécuter ce code

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')
PYTHON

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
PYTHON

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
PYTHON
Un instant

Créez un compte pour exécuter ce code

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
PYTHON
Un instant

Créez un compte pour exécuter ce code

Inscrivez-vous gratuitement pour modifier et exécuter du code Python directement dans votre navigateur.

Bravo, tu es prêt à passer à la suite

Rechercher sur le site

Inscris-toi à Docstring

Pour commencer ton apprentissage.

Tu as déjà un compte ? Connecte-toi.