Pythonのオブジェクトモデルを使いこなしましょう。__eq__と__hash__の契約、NotImplementedという値、コンテキストマネージャー、そしてProtocolによる構造的型付けです。
このレッスンを Kodokon で開くPythonでは、あらゆる演算子、あらゆる変換、あらゆるwith文が特殊メソッドを経由します。a == bはa.__eq__(b)を呼び、len(x)はx.__len__()を呼びます。__repr__ - 開発者に向けた曖昧さのない表現で、デバッガーやリストの中に表示されるもの - と、エンドユーザーに向けた__str__をきちんと区別しましょう。__str__がなければ、Pythonは__repr__にフォールバックします。ですから常に__repr__から先に実装し、できればオブジェクトを再構築できる形にしましょう。
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(値のことです。例外のNotImplementedErrorと混同しないように)を返すと、演算子の交渉プロトコルが働きます。Pythonは次に反射演算other.__eq__(self)を試し、最後の手段としてはじめてFalseと結論します。直接Falseを返すとこの交渉が飛ばされてしまい、実は応答の仕方を知っているサブクラスやサードパーティの型との比較が壊れます。
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")__exit__がTrueを返すと、現在の例外は握りつぶされます - contextlib.suppressはまさにこの仕組みで動いています。クラスを丸ごと書かずに済ませるには、@contextlib.contextmanagerがジェネレーターをコンテキストマネージャーに変えてくれます。yieldより前のコードが__enter__の役割を、そのあと(できればfinallyの中)のコードが__exit__の役割を果たします。オブジェクトモデルのもう一つの柱として、typingモジュールのProtocol型はダックタイピングを形式化します - クラスは正しいメソッドを公開していれば互換とみなされ、継承の宣言はいりません。
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__の中で、比較できない型に出くわしたときは何を返すべきですか?__hash__はそのままで__eq__を追加しました。どうなりますか?__exit__メソッドがTrueを返すことは何を意味しますか?