Python でオブジェクト同士を == で比較したり、辞書のキーや集合の要素として使ったりするとき、裏で動いているのが __eq__ と __hash__ です。この2つは密接に関連していて、片方だけ定義すると問題が起きる。
デフォルトの挙動
何も定義しないと、== は同一オブジェクトかどうか(is と同じ)を判定します。
class User: def __init__(self, name, age): self.name = name self.age = age user1 = User("Alice", 30) user2 = User("Alice", 30) print(user1 == user2) # False(別のオブジェクトだから) print(user1 is user2) # False print(user1 == user1) # True
同じ内容でも別のインスタンスなら False になる。これがデフォルト。
eq で等価性を定義する
__eq__ を定義すれば、「どういう条件で等しいとみなすか」を決められます。
class User: def __init__(self, name, age): self.name = name self.age = age def __eq__(self, other): if not isinstance(other, User): return NotImplemented return self.name == other.name and self.age == other.age user1 = User("Alice", 30) user2 = User("Alice", 30) user3 = User("Bob", 25) print(user1 == user2) # True print(user1 == user3) # False
name と age が同じなら等しい、というルールを定義した。NotImplemented を返すのは、比較できない型のときに Python に処理を委ねるため。
eq だけ定義すると hash が消える
ここで重要な挙動があります。__eq__ を定義すると、デフォルトの __hash__ が無効化されます。
class User: def __init__(self, name, age): self.name = name self.age = age def __eq__(self, other): if not isinstance(other, User): return NotImplemented return self.name == other.name and self.age == other.age user = User("Alice", 30) print(hash(user)) # TypeError: unhashable type: 'User'
辞書のキーや集合の要素にもできなくなる。
users = {user} # TypeError: unhashable type: 'User' cache = {user: "data"} # TypeError
なぜこうなるのか。ハッシュと等価性には重要なルールがあるからです。
ハッシュの不変条件
等しいオブジェクトは同じハッシュ値を持たなければならない。a == b なら hash(a) == hash(b)。
ハッシュ値はオブジェクトの生存期間中に変わってはいけない。
デフォルトの __hash__ は id() に基づくため、__eq__ をオーバーライドするとルール1が破られる可能性がある。だから Python は安全のために __hash__ を無効化するのです。
hash も定義する
__eq__ を定義したら、__hash__ も定義するのが基本。
class User: def __init__(self, name, age): self.name = name self.age = age def __eq__(self, other): if not isinstance(other, User): return NotImplemented return self.name == other.name and self.age == other.age def __hash__(self): return hash((self.name, self.age)) user1 = User("Alice", 30) user2 = User("Alice", 30) print(hash(user1)) # 同じハッシュ値 print(hash(user2)) # 同じハッシュ値 print(hash(user1) == hash(user2)) # True # 辞書のキーや集合の要素として使える users = {user1, user2} print(len(users)) # 1(等しいので1つにまとまる)
__eq__ で使う属性と同じ属性から __hash__ を計算するのがポイント。タプルにまとめて hash() に渡すのが簡単です。
ミュータブルなオブジェクトは要注意
ハッシュ値は変わってはいけない。しかし属性が変更可能だと問題が起きます。
class User: def __init__(self, name): self.name = name def __eq__(self, other): if not isinstance(other, User): return NotImplemented return self.name == other.name def __hash__(self): return hash(self.name) user = User("Alice") users = {user} print(user in users) # True user.name = "Bob" # 属性を変更! print(user in users) # False(ハッシュ値が変わったので見つからない) print(list(users)) # [<User object>](でも中には存在する)
集合の中にいるのに in で見つからない。これはバグの温床になります。
属性を変更不可にする。__setattr__ で書き込みを禁止するか、@dataclass(frozen=True) を使う。
__hash__ = None と明示して、辞書のキーや集合の要素として使えないことを示す。
意図的にハッシュ不可にする
ミュータブルなオブジェクトでは、明示的に __hash__ = None とするのが安全です。
class MutableUser: def __init__(self, name): self.name = name def __eq__(self, other): if not isinstance(other, MutableUser): return NotImplemented return self.name == other.name __hash__ = None # 明示的にハッシュ不可 user = MutableUser("Alice") users = {user} # TypeError: unhashable type: 'MutableUser'
リストや辞書がハッシュ不可なのも同じ理由。内容が変わりうるから。
!= の挙動
__eq__ を定義すれば、!= は自動的にその否定になります。__ne__ を別途定義する必要はありません。
class User: def __init__(self, name): self.name = name def __eq__(self, other): if not isinstance(other, User): return NotImplemented return self.name == other.name user1 = User("Alice") user2 = User("Bob") print(user1 != user2) # True(__eq__ の否定が自動で使われる)
Python 3 では __ne__ のデフォルト実装が __eq__ の否定を返すようになっています。
dataclass を使う
@dataclass を使えば、__eq__ と __hash__ を自動生成できます。
from dataclasses import dataclass @dataclass class User: name: str age: int user1 = User("Alice", 30) user2 = User("Alice", 30) print(user1 == user2) # True print(hash(user1)) # TypeError(デフォルトではハッシュ不可)
デフォルトでは __eq__ だけ生成され、__hash__ は None になる。イミュータブルにするには frozen=True を指定します。
from dataclasses import dataclass @dataclass(frozen=True) class User: name: str age: int user1 = User("Alice", 30) user2 = User("Alice", 30) print(user1 == user2) # True print(hash(user1) == hash(user2)) # True print({user1, user2}) # {User(name='Alice', age=30)} user1.name = "Bob" # FrozenInstanceError(変更不可)
frozen=True なら属性の変更が禁止され、安全に __hash__ が生成される。
まとめ
__eq__ だけ定義すると __hash__ が消える。辞書のキーや集合の要素として使いたいなら __hash__ も定義する。
属性が変わりうるなら __hash__ = None と明示する。または frozen=True でイミュータブルにする。
手動で書くより @dataclass を使う方が安全で簡潔。
__eq__ と __hash__ の関係は、Python のオブジェクトモデルを理解する上で重要なポイントです。両者のルールを守らないと、辞書や集合で予期しないバグが起きる。