0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

#0228(2025/08/28)Pythonの`__hash__`ガイド

Posted at

ハッシュ可能性を理解する: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での扱い

@dataclasseq=Trueが既定。frozen=Trueを付けると不変化が保証され、自動でハッシュ可能になる。frozen=Falseeq=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__との整合性を守ることで、辞書・集合・キャッシュの性能と正しさを両立できる。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?