Pythonの辞書(dict)を理解する：キーの仕組みと「ハッシュ可能(hashable)」の重要性

## はじめに

Pythonにはリスト(list)やタプル(tuple)と並んで、非常に便利で強力な組み込みデータ構造「**辞書 (dictionary, `dict`)**」があります。辞書は「キー(key)」と「値(value)」のペアでデータを管理し、特定のキーを指定して素早く対応する値を取り出すことができます。

他のプログラミング言語では「連想配列」や「ハッシュマップ」などと呼ばれることもありますね。

```python
# 例：名前をキー、電話番号を値とする辞書
phone_book = {'Alice': '080-1111-XXXX', 'Bob': '090-2222-YYYY'}

# キー'Alice'を指定して値を取得
print(phone_book['Alice'])
# 出力: 080-1111-XXXX
```

しかし、`dict`のキーにはどんなデータ型でも使えるわけではありません。そこには「 **ハッシュ可能 (hashable)** 」という重要なルールが関わっています。この記事では、Pythonの辞書の基本的な使い方から、キーに関する制約、そしてその背景にある「ハッシュ可能」という概念について解説します。

## Pythonの辞書(dict)とは？
[Python公式ドキュメント](https://docs.python.org/ja/3/tutorial/datastructures.html#dictionaries)では、辞書は以下のように説明されています。

> 辞書は キー( **`key`** ): 値( **`value`** ) のペアの集合であり、キーが (辞書の中で)一意でなければならない、と考えるとよいでしょう。ある範囲の数でインデクス化されているシーケンスと異なり、辞書は キー ( **`key`** ) でインデクス化されています。

ポイントは以下の3つです。

- キーと値のペア: データは必ずキーとそれに対応する値のセットで格納されます。
- キーによるアクセス: リストのように 0, 1, 2... といった数値のインデックスではなく、指定した「キー」を使って値にアクセスします。
- キーの一意性: 1つの辞書の中で、同じキーを複数持つことはできません。（同じキーで値を設定しようとすると、後から設定した値で上書きされます）
## 基本的な使い方

```python
# 辞書の作成
fruits_prices = {'apple': 100, 'banana': 200}
print(f"初期状態: {fruits_prices}") # 出力: 初期状態: {'apple': 100, 'banana': 200}

# 値へのアクセス (キーを指定)
print(f"りんごの価格: {fruits_prices['apple']}") # 出力: りんごの価格: 100

# 新しいキーと値のペアを追加
fruits_prices['orange'] = 150
print(f"オレンジ追加後: {fruits_prices}") # 出力: オレンジ追加後: {'apple': 100, 'banana': 200, 'orange': 150}

# 既存のキーの値を更新 (キー'apple'の値が上書きされる)
fruits_prices['apple'] = 120
print(f"りんご価格更新後: {fruits_prices}") # 出力: りんご価格更新後: {'apple': 120, 'banana': 200, 'orange': 150}

# キー'banana'のペアを削除
del fruits_prices['banana']
print(f"バナナ削除後: {fruits_prices}") # 出力: バナナ削除後: {'apple': 120, 'orange': 150}
```

# キーが存在するか確認
```python
print(f"'orange'はキーに存在するか？: {'orange' in fruits_prices}") # 出力: True
print(f"'grape'はキーに存在するか？: {'grape' in fruits_prices}")   # 出力: False
```

## `dict`のキーになれるもの、なれないもの
`dict`の非常に重要なルールとして、「 **キーにはハッシュ可能 (hashable) なオブジェクトしか使えない** 」というものがあります。

 **キーになれる型（ハッシュ可能な型）** 

- 数値 (**`int`**, **`float`**, **`complex`**)
- 文字列 (**`str`**)
- ブール値 (**`bool`**) (`True`, `False`)
- **`None`**
- タプル (**`tuple`**): ただし、 **タプルに含まれる全ての要素がハッシュ可能** である場合に限る
- 関数 (**`function`**)
- クラス (**`type`**)
- ユーザ定義オブジェクト（ユーザが定義したクラスのインスタンス、変更可能でもキーにできる）
```python
# キーとして使える型の例
valid_keys_dict = {
    100: "Integer Key",
    3.14: "Float Key",
    "hello": "String Key",
    True: "Boolean Key",
    None: "NoneType Key",
    (1, 'a'): "Tuple Key (all elements immutable)"
}
print(valid_keys_dict) # 出力: {100: 'Integer Key', 3.14: 'Float Key', 'hello': 'String Key', True: 'Boolean Key', None: 'NoneType Key', (1, 'a'): 'Tuple Key (all elements immutable)'}
print(valid_keys_dict[(1, 'a')]) # 出力: Tuple Key (all elements immutable)
```

 **キーになれない型（ハッシュ不能な型）** 

- リスト (**`list`**)
- 辞書 (**`dict`**)
- 集合 (**`set`**)
- **要素にハッシュ不能なオブジェクト（リストなど）を含むタプル**
これらのハッシュ不能なオブジェクトをキーにしようとすると `TypeError` が発生します。

```python
# キーになれない例 (TypeErrorが発生)

# リストをキーにしようとする
try:
    invalid_dict = {[1, 2]: "List Key"}
except TypeError as e:
    print(f"リストをキーにしようとするとエラー: {e}") # 出力: リストをキーにしようとするとエラー: unhashable type: 'list'

# ハッシュ不能な要素(リスト)を含むタプルをキーにしようとする
mutable_list = [10, 20]
try:
    invalid_dict2 = {('a', mutable_list): "Tuple with List Key"}
except TypeError as e:
    print(f"ハッシュ不能な要素を含むタプルをキーにしようとするとエラー: {e}") # 出力: ハッシュ不能な要素を含むタプルをキーにしようとするとエラー: unhashable type: 'list'
```

なぜこのような制約があるのでしょうか？その答えが「 **ハッシュ可能 (hashable)** 」という性質にあります。

## 「ハッシュ可能 (hashable)」とは何か？
「 **ハッシュ可能 (hashable)** 」とは、オブジェクトが以下の性質を持つことを意味します。

- **ハッシュ値 (`hash value`) を持つ** : `hash()` 関数を使って、そのオブジェクト固有の整数値（ハッシュ値）を取得できます。
- ハッシュ値が不変: オブジェクトが存在する間、そのハッシュ値は **常に同じ** でなければなりません。つまり、 **オブジェクトの値が変わらないこと** が前提となります。
- 等しいオブジェクトは同じハッシュ値を持つ: `a == b` が `True` ならば、`hash(a) == hash(b)` も `True` でなければなりません。
```python
# ハッシュ可能なオブジェクト
print(hash('python')) # 出力: -4595009804985601214
print(hash(12345)) # 出力: 12345
print(hash((1, 2, 3))) # 要素がすべて不変なのでOK # 出力: 529344067295497451

# ハッシュ不能なオブジェクト (TypeError)
try:
    print(hash([1, 2, 3])) # 出力: なし
except TypeError as e:
    print(f"リストはハッシュ不能: {e}") # 出力: リストはハッシュ不能: unhashable type: 'list'
```

 **ハッシュ不能なオブジェクト（リストなど）** は、そのハッシュ値が **変わる** 可能性があるため、一貫したハッシュ値を保証できません。そのため、ハッシュ不能とされています。

## なぜ辞書のキーはハッシュ可能でなければならないのか？
辞書がキーを使って高速に値を検索できる秘密は、内部で「 **ハッシュテーブル** 」というデータ構造を利用しているためです。

- 値の格納時: 辞書にキーと値のペアを追加する際、Pythonはまずキーの **ハッシュ値を計算** します。そして、そのハッシュ値に基づいて、値を格納する場所（メモリ上の位置のようなもの）を効率的に決定します。
- 値の検索時: キーを指定して値を検索する際も同様に、まずキーの **ハッシュ値を計算** します。そのハッシュ値を使って、値が格納されていそうな場所に見当をつけ、高速に探し出すことができます。

 **もしキーがハッシュ不能だったらどうなるでしょうか？**
辞書に `{ [1, 2]: 'value' }` のようにリストをキーとして格納できたとします。その後、もしこのリストが `[1, 2, 3]` のように変更されてしまったら、 **ハッシュ値も変わってしまう** （あるいは変わるべき）でしょう。そうなると、辞書はキーに指定したハッシュ値でハッシュテーブルを探しても、そのキー（変更後のリスト）のハッシュ値が存在しないため辞書の値を見つけられなくなってしまいます。

このような混乱を防ぐために、辞書のキーには常に同じハッシュ値を返すことが保証されている **「ハッシュ可能なオブジェクト」しか使えない** というルールになっているのです。

## ハッシュ可能な型・不能な型のまとめ
| ハッシュ可能 (キーになれる) | ハッシュ不能 (キーになれない) |
|:-:|:-:|
| int, float, complex (数値)  | list (リスト)  |
| str (文字列)  | dict (辞書)  |
| bool (True, False)  |   |
| None  |   |
| tuple (要素が全てハッシュ可能な場合のみ)	  | 要素にハッシュ不能なものを含む tuple  |
| frozenset (ハッシュ可能な集合)  | set (集合)  |
| function (関数) | |
| type (クラス) | |
| ユーザー定義オブジェクト  | 一部のユーザー定義オブジェクト |
	
 注意点: **タプル(tuple)はそれ自体はハッシュ可能** ですが、要素としてリストのようなハッシュ不能なオブジェクトを含む場合は、 **タプル全体としてハッシュ不能** になり、辞書のキーには使えません。

 **補足: frozenset** 
`set` と `frozenset` はどちらもPythonにおける集合（重複しない要素のコレクション）を扱うための型ですが、最も重要な違いは **ハッシュ可能**  か **ハッシュ不能 **  かという点です。
 **`frozenset` にハッシュ不能な値（リスト `list`, 辞書 `dict`, 通常の集合 `set` など）を要素として設定することはできません。** 
そのため、 **`frozenset` は`dict`のキーになることができます。** 
```python
# frozenset をいくつか作成します
fs_nums = frozenset([1, 2, 3])
fs_letters = frozenset(["a", "b"])
fs_mixed = frozenset([True, 0, "hello"])
fs_empty = frozenset()  # 空の frozenset

# これらの frozenset をキーとして持つ辞書を作成します
my_dict = {
    fs_nums: "This set contains numbers.",
    fs_letters: "This set contains letters.",
    fs_mixed: "This set contains mixed types.",
    fs_empty: "This is an empty set.",
    # {1, 2, 3}: "Error!" # 通常のsetはキーにできない (TypeErrorになる)
}

# 辞書の内容を表示
print("dictの内容:")
print(my_dict)
print("-" * 20)

# frozenset キーを使って値にアクセスします
print(f"キー {fs_nums} の値:")
print(my_dict[fs_nums])
print("-" * 20)

print(f"キー {fs_letters} の値:")
print(my_dict[fs_letters])
print("-" * 20)

# frozensetは要素の順序を保持せず、同じ要素を持つものは等しいとみなされます
fs_nums_reordered = frozenset([3, 1, 2])
print(
    f"fs_nums == fs_nums_reordered: {fs_nums == fs_nums_reordered}"
)  # True になります
print(f"キー {fs_nums_reordered} でアクセス:")
print(my_dict[fs_nums_reordered])  # fs_nums と同じ値が取得できます
```
 **出力結果**
 ```
dictの内容:
{frozenset({1, 2, 3}): 'This set contains numbers.', frozenset({'b', 'a'}): 'This set contains letters.', frozenset({0, True, 'hello'}): 'This set contains mixed types.', frozenset(): 'This is an empty set.'}
--------------------
キー frozenset({1, 2, 3}) の値:
This set contains numbers.
--------------------
キー frozenset({'b', 'a'}) の値:
This set contains letters.
--------------------
fs_nums == fs_nums_reordered: True
キー frozenset({1, 2, 3}) でアクセス:
This set contains numbers.
 ```

## まとめ
Pythonの辞書(`dict`)は、キーと値のペアでデータを効率的に管理するための強力なデータ構造です。
辞書のキーには「 **ハッシュ可能** 」なオブジェクトしか使用できません。これは、基本的に **変更不能 (immutable)**  なオブジェクトを意味します。ただし、ユーザが定義したクラスのインスタンスは **変更可能(mutable)** なオブジェクトですが、ハッシュ可能なので辞書のキーにできます。
この制約は、辞書が内部でハッシュテーブルを使い、キーのハッシュ値を利用して高速な検索を実現しているためです。
文字列、数値、要素が全て不変なタプルなどはキーに使えますが、リストや辞書、集合などはキーに使えません。
この「 **ハッシュ可能** 」という概念を理解することで、なぜ特定のデータ型が辞書のキーとして使えるのか／使えないのかが明確になり、Pythonの辞書をより深く、そして効果的に活用できるようになるでしょう。