Kodokon kodokon.com

プロトコルと特殊メソッド:__repr__、__eq__、コンテキストマネージャー

Pythonのオブジェクトモデルを使いこなしましょう。__eq__と__hash__の契約、NotImplementedという値、コンテキストマネージャー、そしてProtocolによる構造的型付けです。

9 分 · 3 問

このレッスンを Kodokon で開く

Pythonでは、あらゆる演算子、あらゆる変換、あらゆるwith文が特殊メソッドを経由します。a == ba.__eq__(b)を呼び、len(x)x.__len__()を呼びます。__repr__ - 開発者に向けた曖昧さのない表現で、デバッガーやリストの中に表示されるもの - と、エンドユーザーに向けた__str__をきちんと区別しましょう。__str__がなければ、Pythonは__repr__にフォールバックします。ですから常に__repr__から先に実装し、できればオブジェクトを再構築できる形にしましょう。

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")
未知の型との比較はFalseではなくNotImplementedを返す

NotImplemented(値のことです。例外のNotImplementedErrorと混同しないように)を返すと、演算子の交渉プロトコルが働きます。Pythonは次に反射演算other.__eq__(self)を試し、最後の手段としてはじめてFalseと結論します。直接Falseを返すとこの交渉が飛ばされてしまい、実は応答の仕方を知っているサブクラスやサードパーティの型との比較が壊れます。

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")
Falseを返せば例外は普通に伝播していく

__exit__Trueを返すと、現在の例外は握りつぶされます - contextlib.suppressはまさにこの仕組みで動いています。クラスを丸ごと書かずに済ませるには、@contextlib.contextmanagerがジェネレーターをコンテキストマネージャーに変えてくれます。yieldより前のコードが__enter__の役割を、そのあと(できればfinallyの中)のコードが__exit__の役割を果たします。オブジェクトモデルのもう一つの柱として、typingモジュールのProtocol型はダックタイピングを形式化します - クラスは正しいメソッドを公開していれば互換とみなされ、継承の宣言はいりません。

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は継承を宣言していない。適合は構造的に判定される

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. __eq__の中で、比較できない型に出くわしたときは何を返すべきですか?
    • 不一致をはっきり示すためにFalse
    • NotImplementedErrorを送出する
    • NotImplemented。Pythonにもう一方の被演算子の反射演算を試させるため
    • None。PythonがそれをFalseに変換してくれる
  2. あるクラスに、__hash__はそのままで__eq__を追加しました。どうなりますか?
    • 何も起きない。__hash__はobjectから継承されたまま
    • __hash__がNoneに設定される。インスタンスは辞書のキーや集合の要素として使えなくなる
    • ハッシュが自動的に属性のタプルのハッシュになる
  3. __exit__メソッドがTrueを返すことは何を意味しますか?
    • withブロックがエラーなく終わった
    • コンテキストマネージャーを再利用できる
    • Pythonがwithブロックをもう一度実行する
    • 現在の例外が握りつぶされ、withブロックのあとから実行が再開される