Meistere Pythons Objektmodell: den __eq__/__hash__-Vertrag, den Wert NotImplemented, Kontextmanager und strukturelle Typisierung mit Protocol.
Diese Lektion in Kodokon öffnenIn Python geht jeder Operator, jede Umwandlung, jede with-Anweisung über eine spezielle Methode: a == b ruft a.__eq__(b) auf, len(x) ruft x.__len__() auf. Achte darauf, __repr__ - die eindeutige, an Entwickler gerichtete Darstellung, sichtbar im Debugger und in Listen - von __str__ zu unterscheiden, das für Endnutzer gedacht ist. Fehlt __str__, greift Python auf __repr__ zurück: Implementiere also immer zuerst __repr__, idealerweise in einer Form, mit der du das Objekt rekonstruieren kannst.
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")NotImplemented zurückzugeben (der Wert, nicht zu verwechseln mit der Ausnahme NotImplementedError) stößt das Verhandlungsprotokoll der Operatoren an: Python versucht dann die gespiegelte Operation other.__eq__(self) und schließt erst als letzten Ausweg auf False. Direkt False zurückzugeben würde diese Verhandlung kurzschließen und Vergleiche mit Unterklassen oder Fremdtypen zerbrechen, die in Wirklichkeit sehr wohl zu antworten wüssten.
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")Gibt __exit__ True zurück, wird die aktuelle Ausnahme unterdrückt - genau so funktioniert contextlib.suppress. Um keine ganze Klasse schreiben zu müssen, macht @contextlib.contextmanager aus einem Generator einen Kontextmanager: Der Code vor yield spielt die Rolle von __enter__, der Code danach (idealerweise in einem finally) die von __exit__. Eine weitere Säule des Objektmodells: Die Protocol-Typen aus dem Modul typing formalisieren Duck Typing - eine Klasse ist kompatibel, wenn sie die richtigen Methoden bereitstellt, ohne deklarierte Vererbung.
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__ zurückgeben, wenn du einem Typ gegenüberstehst, mit dem du nicht vergleichen kannst?__eq__ hinzu, ohne __hash__ anzurühren. Was ist die Folge?__exit__-Methode, die True zurückgibt?