Pythonでイミュータブルオブジェクト
Pythonでコレクションを扱っていると、ハッシュ可能なオブジェクトの使用を求められることがままある。
例えば、辞書のキーはハッシュ可能なオブジェクトでないといけないし、集合の要素もハッシュ可能でなければならない。 これはコレクション内部のアルゴリズムにおいて、オブジェクトのハッシュ値を使っているからだろう。
時々、自分で定義したクラスのインスタンスを辞書や集合の中で使いたくなるときがある。そういうとき、そのクラスのインスタンスはハッシュ可能である必要がある。
そこで、ハッシュ可能なインスタンスを生成するクラスは、どのように定義するのが適切なのかについて考えたい。
組み込みのclass⌗
Pythonの組み込みのclass
でクラスを定義すると、そのインスタンスはハッシュ可能である。
つまり、__hash__
メソッドを持っていて、ハッシュ値を計算できる。
class Company:
def __init__(self, cid, name):
self.cid = cid
self.name = name
toyota = Company(1, "Toyota")
hash(toyota) # -9223371913295946368
じゃあ、組み込みのclass
使っておけばOKなのだろうか。 そういうわけにもいかないと思われる。
なぜなら、Pythonの組み込みのclass
のミュータブルな性質が、「プログラム中においてハッシュ値はイミュータブルでなければならない」という要件と相容れないからだ。
公式ドキュメントを読むと、辞書や集合の実装が、ハッシュ可能なオブジェクトのハッシュ値がイミュータブルであることを要求していると書いてある。
しかし、Pythonの組み込みのclass
のインスタンスのフィールドの値は簡単に書き換えることができ、そのインスタンスのハッシュ値がイミュータブルであることは全然保証されていない。
要約すると、ハッシュ可能なオブジェクトはイミュータブルでなければならないが、Pythonの組み込みのclass
のインスタンスはハッシュ可能であるにもかかわらず、ミュータブルになりがちである。
そういうわけで、ユーザー定義クラスをハッシュ可能にしたいならば、組み込みのclass
を使わない方がいいような気がする。
dataclasses⌗
次にdataclasses
モジュールを使う方法がある。
これはバージョン3.7で新たにリリースされた機能らしく、使えるようになったのは割と最近である。
このモジュールを使って新たにクラスを定義するときには、いくつかのパラメータを指定することができる。
そのパラメータの一つに、frozen
がある。
デフォルトではfrozen
はFalse
だが、これをTrue
にしてやると、イミュータブルなインスタンスを生成するクラスを定義することができる。
@dataclass(frozen=True)
class Company:
company_id: int
name: str
そして、frozenなクラスから生成されるインスタンスは、ハッシュ可能である。
frozen=True
にすると、適切な__hash__
メソッドを自動で生成してくれている。
実際にハッシュ可能であるかを確かめてみよう。
@dataclass(frozen=True)
class Company:
company_id: int
name: str
toyota = Company(1, "Toyota")
hash(toyota) #-124679679567354462
set([toyota]) # 集合の要素にできる
{toyota: 1} # 辞書のキーにできる
確かに、Companyのインスタンスはハッシュ可能である。
試しに、上記のfrozen
パラメータをFalse
にしてみると、「toyota
はハッシュ不可能なオブジェクトだ」というエラーが発生する。
つまり、dataclass
を使って普通に定義したクラスのインスタンスは、ミュータブルであるがハッシュ可能ではない。
これは、dataclass
を使ってクラスを定義しておけば、ミュータブルでハッシュ可能なオブジェクトを作らずに済むということだろう。(変なことをしない限り)
とはいえ、先ほどのfrozen
なクラスのインスタンスも完全にイミュータブルであるわけではない。
実はフィールドに辞書がセットされている場合、その辞書の一部の値を書き換えることはできる。
@dataclass(frozen=True)
class Company:
company_id: int
name: str
child: dict
toyota = Company(1, "Toyota", {"company_id": 5, "name": "SUBALU"})
toyota.child["name"] = "SUBARU"
print(toyota)
# Company(company_id=1, name='Toyota', child={'company_id': 5, 'name': 'SUBARU'})
frozenなクラスも「浅く」イミュータブルなだけであるようだ。
しかし、この場合でも、ちゃんとインスタンス自体はハッシュ不可能にしてくれている。
dataclasses
は最近追加された機能だが、構文的にも他の言語でいうところのクラスに近くて良いと思う。
組み込みのクラスを使う場合と比較して、オーバーヘッドがどうなのかというところはちょっと気になるところではあるが、特にパフォーマンスが問題にならない場面では積極的に使わない手はないと思っている。