ハッシュ可能性を理解する:Pythonの__hash__
ガイド
「
__hash__
とは等価性に基づく識別値を計算し、辞書や集合で使えるようにするメソッドである。」
なぜ__hash__
が必要か(概要)
Pythonの辞書や集合はハッシュテーブルで実装され、要素の探索・追加・削除を平均O(1)で行う。この恩恵を受けるには「ハッシュ可能(hashable)」である必要がある。ハッシュ可能とは、hash(x)
が常に同じ整数を返し、x == y
なら常にhash(x) == hash(y)
を満たすこと。__hash__
はその規約をクラス側で実装・制御するためのフックで、堅牢な高速アクセスを実現する鍵だ。
__eq__
との関係とデフォルト挙動
クラスで__eq__
をオーバーライドすると、Pythonは安全のため自動的に__hash__ = None
にし、インスタンスを非ハッシュ化にする。独自の等価性を導入したら、同時に__hash__
の意味も定義し直すのが原則だ。__eq__
を定義しない場合はobject
の実装が使われ、__hash__
はIDベースで計算される。
よく出る比較
観点 | デフォルト(__eq__ 未定義) |
__eq__ のみ定義 |
__eq__ と__hash__ 両方定義 |
---|---|---|---|
ハッシュ可否 | 可 | 不可(TypeError ) |
可 |
ハッシュの意味 | 同一性(id 相当) |
ー | 等価性 |
辞書/集合での挙動 | 可能 | 使用不可 | 等価要素はまとめて扱われる |
正しい__hash__
の書き方パターン
不変(イミュータブル)なフィールドだけを材料にし、タプルへまとめてhash()
に渡すのが基本形。可変フィールド(リスト・辞書・集合等)は原則使わない。
class Point2D:
__slots__ = ("x", "y")
def __init__(self, x: int, y: int):
self.x, self.y = x, y
def __eq__(self, other):
return isinstance(other, Point2D) and (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y))
注意:__hash__
は整数を返す
hash()
は内部でビット演算・リサイズを行うため巨大でも構わないが、int
以外を返すとTypeError
。また、異なるオブジェクトが同じハッシュになること(衝突)は許容されるが、==
がTrue
ならハッシュも同じでなければならない。
使い分けの実践レシピ
1) 実質イミュータブルな値オブジェクト
コンストラクタ以降に状態を変えない前提なら、上記基本形で十分。__slots__
で属性固定すると安全性が増す。
2) 一部が可変だが識別に使わない
可変部をハッシュ計算から外す。等価性も同じ材料に限定する。
class User:
def __init__(self, uid: int, name: str):
self.uid = uid
self.name = name
def __eq__(self, other):
return isinstance(other, User) and self.uid == other.uid
def __hash__(self):
return hash(self.uid)
3) dataclass
での扱い
@dataclass
はeq=True
が既定。frozen=True
を付けると不変化が保証され、自動でハッシュ可能になる。frozen=False
でeq=True
のままだと、__hash__
はNone
になり非ハッシュ化。
from dataclasses import dataclass
@dataclass(frozen=True)
class Token:
kind: str
value: str
# hash(Token(...)) がそのまま使える
4) 識別子だけで等価性を決めるエンティティ
DB主キーやUUIDなど一意キーがある場合、__eq__
/__hash__
をそのキーに揃えると取り回しが良い。
ありがちな落とし穴と対策
-
可変状態をハッシュに入れる:後から値が変わると、集合のバケットがずれて見失う。対策は「不変だけを材料にする」。どうしても変更が必要なら、**
__hash__ = None
**で非ハッシュ化。 -
__eq__
だけ定義して満足:辞書キーに使えなくなる。__eq__
を書いたら__hash__
もセットで。 -
浮動小数点を素材にする:
NaN
は!=
自身という罠。実数値を丸めた整数やDecimal
へ正規化してからハッシュに使うのが無難。 -
順序に意味がない集合を生で入れる:
set
はハッシュ不可。必要ならfrozenset
に変換してから材料にする。
便利テク:アイデンティティ準拠のハッシュ
等価性ではなく「同一性」で辞書キーにしたい場合は、id()
に基づく方法もある。
class IdentityKey:
def __init__(self, obj):
self.obj = obj
def __eq__(self, other):
return isinstance(other, IdentityKey) and id(self.obj) == id(other.obj)
def __hash__(self):
return id(self.obj)
実運用でのチェックリスト
- 等価性の定義は明確か?
- ハッシュ材料は不変か?
-
==
が真ならハッシュは必ず一致するか? - 逆は不要(異なるものがたまたま同ハッシュでも可)。
-
__eq__
を書いたら__hash__
も忘れていないか? - 迷ったら非ハッシュ化して安全を優先。
まとめ
__hash__
は「等価性の定義」と不可分。不変な情報だけから安定した整数を返すよう設計し、__eq__
との整合性を守ることで、辞書・集合・キャッシュの性能と正しさを両立できる。