自作オブジェクトを組み込み型のように振る舞わせる
ふつうのオブジェクトを表示すると <__main__.Position object at 0x7f3a> のようになり、Position("A", 1) == Position("A", 1) は False です。既定では、2つのオブジェクトは同じオブジェクトのときだけ等しいからです。特殊メソッド(アンダースコア2つで囲むので dunder メソッドとも呼ばれます)を使うと、クラスを Python の構文に組み込めます。__repr__ はオブジェクトの表示、__eq__ は ==、__add__ は +、__lt__ は < を制御します。直接呼ぶことはなく、repr(x)、a == b、a + b、sorted(items) を使うと Python が呼び出します。
class Plain:
def __init__(self, cents):
self.cents = cents
a, b = Plain(500), Plain(500)
print(a == b) # False: different objects
print(a.cents == b.cents) # True
print("object at 0x" in str(a)) # True: the default display is unhelpful__repr__:オブジェクトをわかりやすく表示する
__repr__ は、開発者向けにオブジェクトを説明する文字列を返します。慣習として、そのオブジェクトを作るコードのような形にします:Money(1999, 'USD')。リスト、デバッガー、エラーメッセージのすべてがこれを使うので、バグが格段に読みやすくなります。__str__ も定義しない限り、print(x) や str(x) も __repr__ を使います。文字列を表示するのではなく返す必要があり、f 文字列の {self.currency!r} は文字列属性を引用符付きの形にします。
class Money:
def __init__(self, cents, currency="USD"):
self.cents = cents
self.currency = currency
def __repr__(self):
return f"Money({self.cents}, {self.currency!r})"
price = Money(1999)
print(price) # Money(1999, 'USD')
print([Money(5), Money(7, "EUR")]) # [Money(5, 'USD'), Money(7, 'EUR')]__eq__ と __hash__:等しいとはどういうことか
__eq__(self, other) は self == other の結果を決めます。同一性を決めるフィールドを比較し、other が想定した型でないときは False ではなく特殊な値 NotImplemented を返します。すると Python は反対側を試し、最後に自分で False と答えるので、Money(1) == 5 も安全です。__eq__ を定義するとハッシュが無効になり、__hash__ も整合する形で定義しない限り(例:hash((self.cents, self.currency)))、集合の要素や辞書のキーには使えなくなります。
class Money:
def __init__(self, cents, currency="USD"):
self.cents = cents
self.currency = currency
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented # let Python decide
return self.cents == other.cents and self.currency == other.currency
def __hash__(self):
return hash((self.cents, self.currency))
print(Money(500) == Money(500)) # True
print(Money(500) == Money(500, "EUR")) # False
print(Money(500) == 500) # False, no error
print(len({Money(1), Money(1)})) # 1: usable in sets now演算子と順序
__add__(self, other) でクラスに + を持たせられます。self を変更せず新しいオブジェクトを返し、無意味な操作は拒否します。ドルとユーロの加算は ValueError、ただの数値との加算は NotImplemented を返します。__lt__(self, other) で < が使え、sorted()、min()、max() は「a は b より小さいか」を尋ねるだけなので、これだけでオブジェクトを並べ替えられます。__le__、__gt__、__ge__ も追加するか、functools.total_ordering に導出させましょう。
class Money:
def __init__(self, cents, currency="USD"):
self.cents = cents
self.currency = currency
def __repr__(self):
return f"Money({self.cents}, {self.currency!r})"
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented
if other.currency != self.currency:
raise ValueError("cannot add different currencies")
return Money(self.cents + other.cents, self.currency)
def __lt__(self, other):
if not isinstance(other, Money):
return NotImplemented
return self.cents < other.cents
total = Money(150) + Money(275)
print(total) # Money(425, 'USD')
print(sorted([Money(30), Money(10), Money(20)])) # [Money(10, 'USD'), Money(20, 'USD'), Money(30, 'USD')]
print(sum([Money(1), Money(2)], Money(0))) # Money(3, 'USD')__repr__ は __eq__ が見るものをすべて表示するべきです。+ に意外な副作用を持たせず、数値と同じように新しい値を返して両方のオペランドを変更しないようにします。ラボのテストは、元のオブジェクトが変わらないこと、通貨が違うと拒否されること、無関係な型には NotImplemented が返ることを確認します。__repr__は文字列(例:Money(1999, 'USD'))を返す。表示はしない__eq__はフィールドを比較し、無関係な型にはNotImplementedを返す。ハッシュを残すなら__hash__も定義する__add__は新しいオブジェクトを返す。sorted・min・maxには__lt__で十分
次は dataclass です。__init__、__repr__、__eq__ を自動で書いてくれます。
ラボ:採点される内容
__repr__で Money オブジェクトをわかりやすく表示する25点__eq__で2つの金額が等しい条件を決める25点__add__で2つの金額を足す25点__lt__で金額を並べ替えられるようにする25点