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)

あわせて読みたい

プログラミング学習に興味がある方は、まずは無料体験会へ

無料体験会・お申し込みはこちら