高校国語788336 views
教育149510 views
小学算数1200427 views
りんご209490 views
高校日本史190571 views
LaTeX962153 views
中学英語811710 views
小学社会310485 views
中学理科1630641 views
高校生物551766 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 の集合にカスタムオブジェクトを入れる(__hash__ と __eq__)

カスタムクラスのインスタンスを集合に入れるには、__hash__()__eq__() を正しく実装する必要がある。この 2 つのメソッドがハッシュテーブルの動作を決定する。

デフォルトの挙動

カスタムクラスは、何も定義しなければデフォルトでハッシュ可能だ。id() に基づくハッシュ値が使われる。

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

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

s = {p1, p2}
print(len(s))  # 2(同じ座標でも別オブジェクト)

同じ座標を持つ 2 つの Point が、別々の要素として扱われている。これは id() が異なるためだ。

eq を定義すると hash が消える

__eq__() を定義すると、__hash__ は自動的に None になる。

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

p = Point(1, 2)
print(hash(p))  # TypeError: unhashable type: 'Point'

これは安全のための仕様だ。__eq__() で等しいと判定されるオブジェクトは、同じハッシュ値を持たなければならない。デフォルトの id() ベースのハッシュはこの条件を満たさないため、自動的に無効化される。

hasheq の両方を実装

集合に入れるには、両方のメソッドを整合性を持って実装する必要がある。

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

p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)

s = {p1, p2, p3}
print(len(s))  # 2(p1 と p2 は同じ要素)

hash((self.x, self.y)) のようにタプルを使うのが簡単で安全だ。タプルのハッシュ関数が複数の値を適切に混合してくれる。

整合性のルール

__hash__()__eq__() には守るべきルールがある。

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

a == b なら hash(a) == hash(b) でなければならない。逆は成り立たなくてよい。

ハッシュ値は不変

オブジェクトの生存期間中、ハッシュ値が変わってはならない。

違反すると、集合が正しく動作しなくなる。

# 悪い例:ハッシュ値が変わる
class BadPoint:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    
    def __hash__(self):
        return hash((self.x, self.y))

s = set()
p = BadPoint(1, 2)
s.add(p)
print(p in s)  # True

p.x = 10  # ハッシュ値が変わる!
print(p in s)  # False(見つからない)
print(len(s))  # 1(要素は存在する)

ハッシュ値が変わると、正しいスロットを探索できなくなる。

イミュータブルなクラスの設計

安全のため、ハッシュ可能なクラスはイミュータブルにすべきだ。

class ImmutablePoint:
    __slots__ = ('_x', '_y')
    
    def __init__(self, x, y):
        object.__setattr__(self, '_x', x)
        object.__setattr__(self, '_y', y)
    
    @property
    def x(self):
        return self._x
    
    @property
    def y(self):
        return self._y
    
    def __setattr__(self, name, value):
        raise AttributeError("Cannot modify immutable object")
    
    def __eq__(self, other):
        if not isinstance(other, ImmutablePoint):
            return NotImplemented
        return self._x == other._x and self._y == other._y
    
    def __hash__(self):
        return hash((self._x, self._y))

__slots____setattr__ のオーバーライドで変更を防いでいる。

dataclass を使う方法

Python 3.7 以降では dataclass を使うと簡潔に書ける。

from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: int
    y: int

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

s = {p1, p2}
print(len(s))  # 1
print(hash(p1) == hash(p2))  # True

frozen=True を指定すると、イミュータブルなクラスになり、__hash__() も自動生成される。

NamedTuple を使う方法

NamedTuple も選択肢だ。

from typing import NamedTuple

class Point(NamedTuple):
    x: int
    y: int

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

s = {p1, p2}
print(len(s))  # 1

NamedTuple はタプルのサブクラスなので、自動的にハッシュ可能でイミュータブルだ。

dataclass(frozen=True)

柔軟性が高い。メソッドを追加しやすい。

NamedTuple

軽量でシンプル。タプルとの互換性がある。

継承時の注意

サブクラスで属性を追加する場合、ハッシュ関数も更新が必要だ。

@dataclass(frozen=True)
class Point:
    x: int
    y: int

@dataclass(frozen=True)
class Point3D(Point):
    z: int

p1 = Point3D(1, 2, 3)
p2 = Point3D(1, 2, 4)

print(p1 == p2)  # False
print(hash(p1) == hash(p2))  # False(z も含まれる)

dataclass を使えば、継承時も自動的にすべてのフィールドがハッシュに含まれる。

一部の属性だけをハッシュに使う

識別に使わない属性は、ハッシュから除外できる。

from dataclasses import dataclass, field

@dataclass(frozen=True)
class User:
    id: int
    name: str = field(compare=False, hash=False)

u1 = User(1, "Alice")
u2 = User(1, "Bob")

print(u1 == u2)  # True(id のみで比較)
print(hash(u1) == hash(u2))  # True

s = {u1, u2}
print(len(s))  # 1

field(compare=False, hash=False) で、その属性を等価判定とハッシュ計算から除外している。キャッシュや一時的な状態を持つオブジェクトで有用だ。