Maîtrisez le modèle objet de Python : contrat __eq__/__hash__, valeur NotImplemented, context managers et typage structurel avec Protocol.
Ouvrir cette leçon dans KodokonEn 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.
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")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.
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")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é.
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))__eq__, que faut-il renvoyer face à un type que l'on ne sait pas comparer ?__eq__ à une classe sans toucher à __hash__. Quelle est la conséquence ?__exit__ qui renvoie True ?