Computer368106 views
高校倫理1440106 views
高校物理160203 views
数学講師2886227 views
小学算数1200427 views
教育149510 views
高校日本史190571 views
いろは3010814 views
中学英語811710 views
高校化学2924392 views
Help
Tools
NewsSpreadsheetCalendarSlidesTier ListPen ToolIllustrationCrayonPixel ArtASCII ArtPerspectiveEndless StairsGraphMind MapER DiagramFamily TreeMemeCurved TextImage EditorMosaicRetro FilterPencil SketchOCR/HighlighterMakeup EditorFaviconVideo TrimmerScrolling VideoVideo TitleColor PickerColor ExtractorBonfireFireworksWater RippleWater SplashBreaking GlassWood GrainMarble TextureCSS ButtonIcon MakerBar ChartGrouped Bar ChartStacked Bar ChartPie ChartLine ChartArea ChartStacked Area ChartScatter Plot3D Bar Chart3D Pie ChartBar Chart RaceBubble ChartPopulation PyramidPictogramEarningsCandlestick ChartInvestment RiskMortgage SimulatorCalculatorMatrix CalculatorFunction GraphPolynomial ExpansionVenn DiagramField VisualizerRubik's Cube Group TheoryTraveling SalesmanVoronoi and DelaunayFractalColumn ArithmeticDraw Math FiguresArithmetic AnimationArithmetic Word ProblemsCounting with Tree DiagramsCube NetsCross SectionsMotion PathMechanicsWavesLight and LensesThermodynamicsHow Semiconductors WorkMolecular StructuresAtomic OrbitalsElectrochemical CellsChemical EquilibriumCrystal LatticesBuffer pHComplex IonsDNA Double HelixCell DivisionMembrane ChannelsNerve ImpulseRock ClassificationWeatherConstellationsSolar and Lunar Eclipses3D ModelingFloor PlanSeismic StructuresIntersection TurnMaglevCooking AnimationOrigamiLive Viewer CountGeoJSON MapRailway MapPopulation MapCrime MapLand Price MapSchool MapShrine and Castle MapHouse of Representatives MapWord MapSolitaireReversiHakoiri MusumeChessHamburgerRippleSlide Puzzle MakerNeon PinballNovel MakerJapanese Typing PracticePiano Score EditorMusic Theory

English

Python 辞書のハッシュテーブル実装:オープンアドレス法と hashable の意味

Python の辞書がなぜ高速なのか、その秘密はハッシュテーブルという仕組みにあります。キーを指定して値を取り出す操作は、要素数がいくら増えても平均 O(1) の計算量で完了します。この記事では、CPython がどのようにハッシュテーブルを実装しているのか、衝突をどう解決しているのか、そしてなぜキーには「hashable」な値しか使えないのかを解説します。

ハッシュテーブルの基本的な考え方

ハッシュテーブルは、キーから「ハッシュ値」と呼ばれる整数を計算し、その値を使って配列のどの位置にデータを格納するかを決める仕組みです。

# Python でハッシュ値を確認する
print(hash("apple"))   # 例: 2324522789372603423
print(hash("banana"))  # 例: -8872142557351543024
print(hash(42))        # 42(小さい整数は自身がハッシュ値)

ハッシュ値がわかれば、配列のインデックスは hash(key) % table_size のような計算で求められます。リストのように先頭から順に探す必要がないため、要素数に関係なく一定時間でアクセスできるわけです。

オープンアドレス法による衝突解決

異なるキーが同じインデックスを指すことを「衝突」と呼びます。たとえば、テーブルサイズが 8 のとき、hash(key) % 8 が同じ値になるキーが複数存在する可能性があります。

衝突を解決する方法は大きく分けて 2 つあります。

チェイン法

各スロットにリンクリストを持たせ、衝突したキーをリストに追加していく。実装が単純だが、リスト走査のオーバーヘッドがある。

オープンアドレス法

衝突したら別の空きスロットを探して格納する。CPython はこちらを採用している。メモリ局所性が良く、キャッシュ効率が高い。

CPython が採用しているオープンアドレス法では、最初に計算したインデックスが埋まっていたら、一定の規則に従って次の候補スロットを探します。この「次を探す」処理をプロービング(probing)と呼びます。

CPython のプロービング手法

単純に「次のスロット」を見ていく線形プロービングでは、連続した領域が埋まりやすく、性能が劣化します。CPython は擬似ランダムプロービングを採用しており、ハッシュ値の全ビットを活用して次の候補位置を決定します。

# CPython の probing 計算(概念的な疑似コード)
perturb = hash_value
index = perturb % table_size

while slot_is_occupied(index):
    perturb >>= 5
    index = (index * 5 + perturb + 1) % table_size

perturb 変数がハッシュ値の上位ビットを順次取り込むことで、同じ初期インデックスを持つキーでも異なる経路でスロットを探索します。これにより、クラスタリング(連続した領域が埋まる現象)を防ぎ、検索効率を維持しています。

負荷率とリサイズ

ハッシュテーブルが埋まりすぎると、空きスロットを見つけるまでのプロービング回数が増え、性能が悪化します。CPython は負荷率(使用中スロット数 ÷ テーブルサイズ)が約 2/3 を超えると、テーブルサイズを拡張してすべてのエントリを再配置します。

import sys

d = {}
print(sys.getsizeof(d))  # 64(空の辞書)

for i in range(10):
    d[i] = i
print(sys.getsizeof(d))  # 352(リサイズ後)

リサイズは O(n) のコストがかかりますが、頻繁には発生しないため、償却計算量としては O(1) が維持されます。

なぜキーは hashable でなければならないのか

ハッシュテーブルが正しく機能するには、同じキーは常に同じハッシュ値を返す必要があります。もしキーの中身が変わってハッシュ値が変わると、以前格納した場所を見つけられなくなってしまいます。

# リストはハッシュ化できない
try:
    hash([1, 2, 3])
except TypeError as e:
    print(e)  # unhashable type: 'list'

# タプルはハッシュ化できる(中身がすべて hashable なら)
print(hash((1, 2, 3)))  # 529344067295497451

Python では、オブジェクトが hashable であるための条件は次のとおりです。

ライフタイム中にハッシュ値が変わらない

__hash__() メソッドが常に同じ値を返す必要があります。可変オブジェクトはこの条件を満たせません。

等価比較が可能

__eq__() メソッドが定義されている必要があります。ハッシュ値が同じでも実際のキーが異なる場合があるため、等価比較で最終確認を行います。

等しいオブジェクトは同じハッシュ値を持つ

a == b なら hash(a) == hash(b) でなければなりません。逆は成り立たなくても構いません(衝突は許容される)。

リストや辞書、集合といった可変オブジェクトは、中身が変わるとハッシュ値も変わってしまうため、意図的に __hash__ が定義されていません。

カスタムクラスを辞書のキーにする

自作クラスのインスタンスをキーにしたい場合、__hash____eq__ を適切に実装する必要があります。

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __hash__(self):
        return hash((self.x, self.y))
    
    def __eq__(self, other):
        return isinstance(other, Point) and self.x == other.x and self.y == other.y

p1 = Point(1, 2)
p2 = Point(1, 2)

d = {p1: "first"}
print(d[p2])  # "first"(p1 == p2 かつ hash(p1) == hash(p2) なので同じキー扱い)

注意点として、__eq__ をオーバーライドすると、デフォルトの __hash__id() ベース)は無効化されます。__hash__ を明示的に定義しないと TypeError が発生するため、両方をセットで実装するのが基本です。

ハッシュ値の衝突と等価比較

ハッシュ値が同じでも、キーが等しいとは限りません。この状況を処理するため、CPython は以下の手順でキーを照合します。

ハッシュ値を比較

一致したら == で等価比較

等しければ同一キーと判定

ハッシュ値の比較は整数同士の比較なので非常に高速です。等価比較は重い処理になりうるため、ハッシュ値が異なれば即座に「別のキー」と判断できる仕組みになっています。

# ハッシュ値が同じでも等しくない例
class AlwaysSameHash:
    def __init__(self, value):
        self.value = value
    def __hash__(self):
        return 1  # 常に同じハッシュ値
    def __eq__(self, other):
        return isinstance(other, AlwaysSameHash) and self.value == other.value

a = AlwaysSameHash("A")
b = AlwaysSameHash("B")

print(hash(a) == hash(b))  # True
print(a == b)              # False

d = {a: 1, b: 2}
print(len(d))  # 2(別々のキーとして格納される)

この例では全キーが同じインデックスに衝突しますが、等価比較で区別されるため正しく動作します。ただし、プロービングが頻発するため性能は大幅に悪化します。

まとめ

Python の辞書は、ハッシュテーブルとオープンアドレス法を組み合わせることで、高速なキー検索を実現しています。キーが hashable でなければならない理由は、ハッシュ値が変わると格納場所を見失ってしまうからです。カスタムクラスをキーにする際は、__hash____eq__ を正しく実装し、「等しいオブジェクトは同じハッシュ値を持つ」という原則を守ることが重要です。