Python __hash__とは?setや辞書のキーに使えるクラスを作る方法
__hash__は、オブジェクトをsetに入れたり辞書のキーにしたりできるようにする特殊メソッドです。
多くの場合、__eq__を書いた直後に「書かざるを得なくなる」メソッドでもあります。
__eq__を書くとhashが消える
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)
print(Point.__hash__)
{Point(1, 2)}
実行結果:
None
TypeError: unhashable type: 'Point'
__eq__を定義すると、Pythonは__hash__を自動的にNoneに設定します。
これは意地悪ではなく、壊れた状態を防ぐための安全装置です。等価の定義を変えたのにハッシュ値が元のままだと、setや辞書が正しく動かなくなるからです。
__hash__の基本
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y))
print(len({Point(1, 2), Point(1, 2), Point(3, 4)}))
print({Point(1, 2): "原点の近く"}[Point(1, 2)])
実行結果:
2
原点の近く
比較に使ったのと同じ値をタプルにまとめてhash()に渡す——これが定番の書き方です。
自分でハッシュ値を計算する必要はありません。タプルのhash()が中身を組み合わせてくれます。
守るべきルール
ハッシュには1つだけ絶対のルールがあります。
a == bならばhash(a) == hash(b)
逆は成り立たなくて構いません。ハッシュ値が同じでも中身が違うこと(衝突)は許されます——そのときは__eq__で最終判定されます。
このルールを守る最も簡単な方法が、__eq__と__hash__で同じ属性を使うことです。
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y))
片方だけself.zを足す、といったズレがバグの温床になります。
変わる値をキーにしてはいけない
class Name:
def __init__(self, n):
self.n = n
def __eq__(self, other):
return isinstance(other, Name) and self.n == other.n
def __hash__(self):
return hash(self.n)
key = Name("a")
d = {key: "値"}
key.n = "b"
print(key in d)
print(list(d.values()))
実行結果:
False
['値']
値は辞書に残っているのに、キーで取り出せなくなりました。
格納時のハッシュ値と、書き換え後のハッシュ値が食い違うためです。辞書は元の場所を探しに行きますが、そこにはもう見つかりません。
だからこそハッシュ可能にするクラスは、変更できない設計にすべきです。
frozen dataclassが最も安全
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
print(len({Point(1, 2), Point(1, 2)}))
実行結果:
1
frozen=Trueを付けると、__eq__と__hash__の両方が自動生成され、しかも属性の書き換えが禁止されます。
前の節の事故が原理的に起きません。辞書のキーやsetに入れたい値クラスは、まずこれを検討してください。
なおfrozenを付けない@dataclassはハッシュ不可です。
@dataclass
class G:
n: str
{G("a")}
実行結果:
TypeError: unhashable type: 'G'
詳しくは「Python dataclassのfrozen=Trueとは?変更できないクラスを作る方法」で解説しています。
既定のhashは何を見ているか
__eq__も__hash__も書かないクラスは、そのままハッシュ可能です。
class R:
pass
r = R()
print(hash(r) == id(r) // 16)
実行結果:
True
既定のハッシュはオブジェクトのアドレス(id)から作られています。中身は見ていません。
そのため、中身が同じでも別オブジェクトなら別のハッシュ値になります。既定の==がisと同じ判定なのと、きれいに対応しています。
※ // 16という関係はCPythonの実装上のもので、仕様として保証されたものではありません。
__hash__のまとめ
set・辞書のキーに使えるようにする__eq__を書くと__hash__はNoneになる- 比較と同じ属性をタプルにして
hash()へ - ルールは
a == b→hash(a) == hash(b) - 書き換わる属性をハッシュに使わない——取り出せなくなる
- 迷ったら
@dataclass(frozen=True)




