Kodokon kodokon.com

Protocoles et méthodes spéciales : __repr__, __eq__, context managers

Maîtrisez le modèle objet de Python : contrat __eq__/__hash__, valeur NotImplemented, context managers et typage structurel avec Protocol.

9 min · 3 questions

Ouvrir cette leçon dans Kodokon

En Python, chaque opérateur, chaque conversion, chaque instruction with passe par une méthode spéciale : a == b appelle a.__eq__(b), len(x) appelle x.__len__(). Distinguez bien __repr__ - représentation non ambiguë destinée au développeur, affichée dans le débogueur et à l'intérieur des listes - de __str__, destinée à l'utilisateur final. Si __str__ est absente, Python retombe sur __repr__ : implémentez donc toujours __repr__ en premier, idéalement sous une forme qui permet de reconstruire l'objet.

PYTHON
class Money:
    def __init__(self, amount: int, currency: str):
        self.amount = amount
        self.currency = currency

    def __repr__(self) -> str:
        return f"Money({self.amount!r}, {self.currency!r})"

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Money):
            return NotImplemented
        return (self.amount, self.currency) == (
            other.amount, other.currency)

print(Money(10, "EUR") == Money(10, "EUR"))
print(Money(10, "EUR") == "10 EUR")
Comparer à un type inconnu renvoie NotImplemented, jamais False.

Renvoyer NotImplemented (la valeur, à ne pas confondre avec l'exception NotImplementedError) déclenche le protocole de négociation des opérateurs : Python essaie alors l'opération réfléchie other.__eq__(self), et ne conclut à False qu'en dernier recours. Renvoyer False directement court-circuiterait cette négociation et casserait les comparaisons avec des sous-classes ou des types tiers qui, eux, sauraient répondre.

PYTHON
import time

class Timer:
    def __enter__(self):
        self.start = time.perf_counter()
        return self

    def __exit__(self, exc_type, exc, tb):
        stop = time.perf_counter()
        self.elapsed = stop - self.start
        return False

with Timer() as timer:
    sum(range(1_000_000))
print(f"{timer.elapsed:.4f} s")
Renvoyer False laisse toute exception se propager normalement.

Si __exit__ renvoie True, l'exception en cours est supprimée - c'est exactement ainsi que contextlib.suppress fonctionne. Pour éviter d'écrire une classe complète, @contextlib.contextmanager transforme un générateur en context manager : le code avant yield joue le rôle de __enter__, le code après (idéalement dans un finally) celui de __exit__. Autre pilier du modèle objet : les Protocol du module typing formalisent le duck typing - une classe est compatible si elle expose les bonnes méthodes, sans héritage déclaré.

PYTHON
from typing import Protocol, runtime_checkable

@runtime_checkable
class Closable(Protocol):
    def close(self) -> None: ...

class Session:
    def close(self) -> None:
        print("session closed")

def shutdown(resource: Closable) -> None:
    resource.close()

shutdown(Session())
print(isinstance(Session(), Closable))
Session ne déclare aucun héritage : la conformité est structurelle.

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. Dans __eq__, que faut-il renvoyer face à un type que l'on ne sait pas comparer ?
    • False, pour signaler clairement l'inégalité
    • Lever NotImplementedError
    • NotImplemented, pour laisser Python tenter l'opération réfléchie de l'autre opérande
    • None, que Python convertira en False
  2. Vous ajoutez __eq__ à une classe sans toucher à __hash__. Quelle est la conséquence ?
    • Aucune : __hash__ reste hérité de object
    • __hash__ est mis à None : les instances ne peuvent plus servir de clés de dict ni d'éléments de set
    • Le hash devient automatiquement celui du tuple des attributs
  3. Que signifie une méthode __exit__ qui renvoie True ?
    • Le bloc with s'est terminé sans erreur
    • Le context manager pourra être réutilisé
    • Python relancera le bloc with une seconde fois
    • L'exception en cours est supprimée et l'exécution reprend après le bloc with