Python初心者がマスターしておきたい辞書の活用テクニック10選！

# はじめに
みずほリサーチ&テクノロジーズの @fujine です。

本記事では、**Pythonの辞書をより便利に使うためのテクニック10個を解説**していきます。基本的な操作も多く含まれていますが、
- パフォーマンスを意識して実装する
- キーの数に依存せず、短い行数で簡潔に実装する
- 辞書の可読性を高める 

ことに重点を置きました。**Pythonの辞書に一通り慣れた初心者が、チーム開発やより複雑なプログラム開発へステップアップする際に活用**いただけたらと思います。

1. [辞書を作成する](#辞書を作成する)
1. [複数の辞書を１つに結合する](#複数の辞書を１つに結合する)
1. [初期値を持つ辞書を作成する](#初期値を持つ辞書を作成する)
1. [辞書の要素を取得・更新・追加・削除する](#辞書の要素を取得更新追加削除する)
1. [要素の追加と取得を一度に実行する](#要素の追加と取得を一度に実行する)
1. [辞書から複数の要素を同時に取得する](#辞書から複数の要素を同時に取得する)
1. [辞書をキーや値でソートする](#辞書をキーや値でソートする)
1. [辞書を読み取り専用にする](#辞書を読み取り専用にする)
1. [カスタム辞書を作成する](#カスタム辞書を作成する)
1. [型ヒントを導入する](#型ヒントを導入する)


実行環境はWindows10(64bit) 22H2、Pythonバージョンは以下の通りです。
```sh
$ python -V
Python 3.12.0
```

# 辞書を作成する
波括弧`{}`、もしくは組み込み関数`dict()`で辞書を作成できます。
```python
d = {"a": 1, "b": 3.14, "c": []}
```

```python
d = dict(a=1, b=3.14, c=[])
```

:::note info

辞書の作成時間は **`{}`の方が3倍近く高速** です。大量の辞書を作成する場合やパフォーマンス重視のアプリケーションでは、`{}`を使用しましょう。

```python
>python -m timeit "{}"
1000000 loops, best of 5: 183 nsec per loop
>python -m timeit "dict()"
500000 loops, best of 5: 617 nsec per loop
```

:::

辞書内包表記でも辞書を作成できます。リスト等のシーケンスオブジェクトを辞書に変換する時に便利です。

```python
keys = ["a", "b", "c"]
values = [1, 3.14, None]

{k: v for k, v in zip(keys, values)}  # {'a': 1, 'b': 3.14, 'c': None}
```

:::note warn

辞書と集合（`set`）はどちらも同じ波括弧（`{}`）を使用するため、辞書内包表記にしたつもりが集合内包表記とならないよう注意しましょう。
```python
d = {k for k, v in zip(keys, values)}  # {k: v for ...}のタイプミス例
d["a"]  # TypeError: 'set' object is not subscriptable
```
:::

# 複数の辞書を１つに結合する
複数の辞書を結合して、新たな辞書を生成できます。
説明にあたり、結合前の辞書を3つ用意します。

```python
d1 = {"a": 1, "b": 3.14}
d2 = {"b": 2.714, "c": None}
d3 = {"d": []}
```

`|`演算子で辞書同士を結合した新たな辞書オブジェクトを生成できます。キーが重複する場合、後から結合した値で上書きされます。3つ以上の結合も可能です。
```python
d1 | d2  # {'a': 1, 'b': 2.714, 'c': None}

d2 | d1  # {'b': 3.14, 'c': None, 'a': 1}

d1 | d2 | d3  # {'a': 1, 'b': 2.714, 'c': None, 'd': []}
```

:::note warn
`|`演算子による結合はPython3.9から利用可能です。それ以前のバージョンでは、以下の実装でも同じ結果を得ることができます。
```python
{**d1, **d2}  # {'a': 1, 'b': 2.714, 'c': None}
```
:::

[update](https://docs.python.org/ja/3/library/stdtypes.html#dict.update)メソッドでも結合可能です。`|`演算子との違いとして、`update`はインプレース操作です。また、キーが重複した場合は`TypeError`が送出されます。
```python
d = {}
d.update(**d1, **d2)  # TypeError: dict.update() got multiple values for keyword argument 'b'
```

:::note info
- 予期しない上書きを検知したい場合は`update`
- 上書きを許容する場合は`|`演算子

という使い分けが良さそうです。
:::

# 初期値を持つ辞書を作成する
よくある操作の１つに、「辞書に特定のキーが存在しない場合、そのキーと値（初期値）を設定する」があります。

以下のように愚直に代入することもできますが、
- キーの数が多いとコードが長くなり保守しづらい
- 処理結果の辞書はキー順序が不定になる
 
という問題があります。
```python
d_input = {"b": 2.714, "d": []}

if "a" not in d_input:
    d_input["a"] = 1


if "b" not in d_input:
    d_input["b"] = 3.14


if "c" not in d_input:
    d_input["c"] = None

# 本当はa, b, c, dのキー順にしたい
d_input  # {'b': 2.714, 'd': [], 'a': 1, 'c': None}
```

代わりに、初期値を持った`d_init`を定義して入力データ`d_input`とマージしましょう。これなら、**初期値が複数ある場合も一括設定できる上、キーの順序もコントロールできます**。
```python
d_init = {"a": 1, "b": 3.14, "c": None}

d_init | d_input  # {'a': 1, 'b': 2.714, 'c': None, 'd': []}
```

全てのキーで初期値が同じ場合は、[fromkeys](https://docs.python.org/ja/3/library/stdtypes.html#dict.fromkeys)クラスメソッドを使いましょう。第1引数にキーのシーケンスオブジェクト、第2引数に初期値の値（省略時は`None`）を設定することで、**全てのキーの同じ値の辞書を生成**できます。

```python
keys = ("a", "b", "c")
{}.fromkeys(keys)     # {'a': None, 'b': None, 'c': None}
{}.fromkeys(keys, 0)  # {'a': 0, 'b': 0, 'c': 0}
```

パフォーマンスは、辞書内包表記と`fromkeys`のどちらも同程度です。
```sh
>python -m timeit "{k: None for k in range(100000)}"
5 loops, best of 5: 79.6 msec per loop
>python -m timeit "{}.fromkeys(range(100000))"     
5 loops, best of 5: 81.5 msec per loop
```

:::note warn
`fromkeys`の初期値はイミュータブルオブジェクトのみを指定して下さい。リストや辞書のようなミュータブルオブジェクトを指定すると、辞書の値は全て同じオブジェクトを参照するようになります。

```python
d = {}.fromkeys(("a", "b", "c"), [])   # 初期値にリストを指定
d["a"] is d["b"] is d["c"]             # True
```

ミュータブルオブジェクトを初期値にしたい場合は、代わりに辞書内包表記を使用しましょう。
```python
d = {k: [] for k in ("a", "b", "c")}
d["a"] is d["b"] is d["c"]            # False
```
:::

# 辞書の要素を取得・更新・追加・削除する
辞書の基本的な取得、更新、追加、削除の操作は以下の通りです。

```python
d = {"a": 1}

# 取得
d["a"]  # 1

# 更新
d["a"] *= 2
d["a"]  # 2

# 追加
d["b"] = 3.14

# 削除
del d["a"]
"a" in d  # False
```

存在しないキーを取得しようとすると`KeyError`が送出されます。
[get](https://docs.python.org/ja/3/library/stdtypes.html#dict.get)メソッドを使えば、キーが存在しない場合はデフォルト値（省略時は`None`）が応答され、例外は発生しません。
```python
d["a"]              # KeyError: 'a'
d.get("a") is None  # True
d.get("a", 1)       # 1
```

存在しないキーを削除しようとしても`KeyError`が送出されます。
[pop](https://docs.python.org/ja/3/library/stdtypes.html#dict.pop)メソッドを使うと、キーが存在しない場合は代わりにデフォルト値が応答されます。
```python
del d["a"]     # KeyError: 'a'
d.pop("a", 1)  # 1
```

:::note warn
`get`と異なり、`pop`ではキーが存在せずデフォルト値も指定されない場合は`None`ではなく`KeyError`が送出されます。`pop`ではデフォルト値を指定しましょう。

```python
d.pop("a")  # KeyError: 'a'
```
:::

# 要素の追加と取得を一度に実行する
よくある操作の1つに、「特定のキーがあれば値を取得し、なければキーと初期値を追加してから値を取得する」というのもあります。

以下にキー名と初期値を定義します。
```python
d = {}
key = "a"
default = 1
```

よくある実装例は以下です。`if`文でキーの有無を判定して値を追加しています。
```python
if key not in d:
    d[key] = default

d[key]  # 1
```

[setdefault](https://docs.python.org/ja/3/library/stdtypes.html#dict.setdefault)メソッドを使えば、同じ処理を1行でシンプルに実装できます。**`if`文は必要ありません。**
```python
d.setdefault(key, default)  # 1
```

:::note info
`setdefault`は便利ですが、`set`なのに`get`もしており、命名があまり直感的ではありません。私は`setdefault → set default (and get)`という独自の覚え方をしています。
:::

# 辞書から複数の要素を同時に取得する
通常、辞書に指定可能なキーは一度に1つのみです。`d[key]`では複数の値を同時に指定・取得することはできません。
```python
d = {"a": 1, "b": 3.14, "c": []}

d["a"]       # 1
d["a", "b"]  # KeyError: ('a', 'b')
```

[operator.itemgetter](https://docs.python.org/ja/3/library/operator.html#operator.itemgetter)を使えば、**複数の値を同時に取得することが可能**です。`itemgetter`の引数にキーを複数指定すると、指定したキーの順番で値を取得できます。

```python
from operator import itemgetter

itemgetter("a")(d)            # 1
itemgetter("a", "b")(d)       # (1, 3.14)
itemgetter("c", "b", "a")(d)  # ([], 3.14, 1)
```

これを応用すれば、辞書から特定のキーのみをフィルタリングすることも簡単にできます。
```python
from string import ascii_lowercase

# キーが英小文字、値が英大文字の辞書（長さ26）を作成
d = {k: k.upper() for k in ascii_lowercase}  

# 検索キーをa, j, eとし、検索キーの順序を保持しつつフィルタリング
keys = ("a", "j", "e")
values = itemgetter(*keys)(d)
dict(zip(keys, values))  # {'a': 'A', 'j': 'J', 'e': 'E'}
```


# 辞書をキーや値でソートする
辞書`d`に対し、`sorted(d)`では昇順ソートされたキー名だけが出力されます（これは`sorted(d.keys())`と同じです）。
`sorted(d.items())`とすれば、キーと値の両方が出力されます。この場合、`d.items()`によりキー・値のペア`(key, value)`が展開され、その0番目である`key`がソート対象となります。

```python
d = {"a": 1, "c": 0, "b": -1}
sorted(d)          # ['a', 'b', 'c']
sorted(d.items())  # [('a', 1), ('b', -1), ('c', 0)]
```

この仕組みを利用し、辞書内包表記と`dict()`の2通りでキー順によるソートが可能です。
```python
# キーで昇順ソート
{k: d[k] for k in sorted(d)}  # {'a': 1, 'b': -1, 'c': 0}
dict(sorted(d.items()))       # {'a': 1, 'b': -1, 'c': 0}

# キーで降順ソート
{k: d[k] for k in sorted(d, reverse=True)}  # {'c': 0, 'b': -1, 'a': 1}
dict(sorted(d.items(), reverse=True))       # {'c': 0, 'b': -1, 'a': 1}
```

値でソートする場合は、`sorted`の`key`引数を用います。`lambda`関数か`itemgetter(1)`を使用して、値をソート対象とします。
```python
# 値で昇順ソート
{k: d[k] for k in sorted(d, key=lambda x: d[x])}  # {'b': -1, 'c': 0, 'a': 1}
dict(sorted(d.items(), key=lambda x: x[1]))       # {'b': -1, 'c': 0, 'a': 1}
dict(sorted(d.items(), key=itemgetter(1)))        # {'b': -1, 'c': 0, 'a': 1}

# 値で降順ソート
{k: d[k] for k in sorted(d, key=lambda x: d[x], reverse=True)}  # {'a': 1, 'c': 0, 'b': -1}
dict(sorted(d.items(), key=lambda x: x[1], reverse=True))       # {'a': 1, 'c': 0, 'b': -1}
dict(sorted(d.items(), key=itemgetter(1), reverse=True))        # {'a': 1, 'c': 0, 'b': -1}
```

パフォーマンスを比較すると、キーと値のどちらでソートする場合も、辞書内包表記の方が速い結果となりました。これは、`d.items()`でキーと値のペアを生成する処理コストの方が大きいためと考えられます。

```sh
>python -m timeit -s "d = {i: i for i in range(100000, 0, -1)}" "{k: d[k] for k in sorted(d)}"
20 loops, best of 5: 12.3 msec per loop
>python -m timeit -s "d = {i: i for i in range(100000, 0, -1)}" "dict(sorted(d.items()))"
5 loops, best of 5: 94.2 msec per loop

>python -m timeit -s "d = {i: i for i in range(100000, 0, -1)}" "{k: d[k] for k in sorted(d, key=lambda x: d[x])}"
10 loops, best of 5: 20.6 msec per loop
>python -m timeit -s "from operator import itemgetter; d = {i: i for i in range(100000, 0, -1)}" "dict(sorted(d.items(), key=itemgetter(1)))"
2 loops, best of 5: 99.3 msec per loop
```

# 辞書を読み取り専用にする
[types.MappingProxyType](https://docs.python.org/ja/3/library/types.html#types.MappingProxyType)を使うことで、辞書の読み取り専用ビューを作成することができます。ビューに対しては参照操作のみが可能です。

```python
from types import MappingProxyType

d = {"a": 1}
view = MappingProxyType(d)

# 参照操作は通常の辞書と同じ
"a" in view          # True
view["a"]            # 1
view.get("b", 3.14)  # 3.14
view.items()         # dict_items([('a', 1)])

# ビューに対する更新・追加・削除はTypeError
view["a"] = 2     # TypeError: 'mappingproxy' object does not support item assignment
view["b"] = 3.14  # TypeError: 'mappingproxy' object does not support item assignment
del view["a"]     # TypeError: 'mappingproxy' object does not support item deletion

# ビューに対する和集合演算は、新たな辞書オブジェクトを生成する
view | {"b": 3.14}  # {'a': 2, 'b': 3.14}

# マッピング元オブジェクトの変更はビューにも反映される
d["a"] = 2
view["a"]   # 2
```

:::note info
更新・追加・削除操作は`TypeError`となるため、**「辞書オブジェクトへの予期しない変更」をガードしたい場面で効果的**です。
:::

# カスタム辞書を作成する
辞書に独自の処理をカスタムしたい場合は、[collections.UserDict](https://docs.python.org/ja/3/library/collections.html#collections.UserDict)を使用します。

例えば、`KeyError`発生時に現在のキー情報を例外メッセージに加えた`FriendlyDict`クラスを定義してみます。`UserDict`を継承し、キーが存在しない場合に呼び出される`__missing__()`特殊メソッド内の例外メッセージをオーバーライドします。

```python
from collections import UserDict

class FriendlyDict(UserDict):
    def __missing__(self, key):
        raise KeyError(f"'{key}' not found, keys are {tuple(self.keys())}")
```

存在しないキーでアクセスすると、期待通りの例外メッセージを得ることができました。既存のキー情報を出力することで、デバッグの効率化にもつながりそうです。
```python
fd = FriendlyDict(a=1, b=3.14)
fd["c"]  # KeyError: "'c' not found, keys are ('a', 'b')"
```

:::note warn
`UserDict`の代わりに、組み込みクラスである`dict`を継承する方法もあります。`UserDict`を使用したのは、特殊メソッドのオーバーライドによる他特殊メソッドへの意図しない影響を最小化するためです。

:::

# 型ヒントを導入する
辞書に型ヒントを定義すると、想定する型や値を明示することができ、プログラムの可読性が向上します。また、型ヒントチェッカーにより想定外データの混入が検知できるため、バグや不良を早期発見しやすくなります。

:::note warn
pythonの型ヒントは静的解析時のみ検査されるため、プログラム実行時には検査されず、エラーも発生しません。
実行時にも型を検査したい場合は、[pydantic](https://docs.pydantic.dev/latest/)などの外部パッケージをご使用下さい。
:::

型ヒントチェッカーとして、[mypy](https://mypy.readthedocs.io/en/stable/index.html)をインストールします。

```sh
$ pip install mypy
```

まずはシンプルに、`変数: dict`としてみましょう。この変数に辞書以外のオブジェクトを代入するとチェックエラーになります。
```python
d: dict = {}  # OK
d: dict = 1   # error: Incompatible types in assignment
```

続いて、辞書のキーと値のデータ型を指定します。任意のデータ型は`Any`、複数のデータ型を許容する場合は`|`で連結します。

```python
from typing import Any

d: dict[str, Any] = {"a": 1}  # OK
d: dict[str, Any] = {1: 1}    # error: Dict entry 0 has incompatible type "int": "int"; expected "str": "Any"

d: dict[str, int | float] = {"a": 1, "b": 3.14}  # OK
d: dict[str, int | None] = {"a": 1, "c": None}   # OK
d: dict[str, int | float] = {"c": None}          # error: Dict entry 0 has incompatible type "str": "None"; expected "str": "int | float"
```

キーや値に特定のリテラル値のみを許容するには[Literal](https://docs.python.org/ja/3/library/typing.html#typing.Literal)を使用します。指定したリテラル値以外の代入はチェックエラーになります。`Literal`に`float`や`Any`、変数や式を指定することはできません。
なお`AllowedKeys`と`AllowedValues`は辞書の全要素に適用されるため、特定のキーに特定のリテラル値を制約付けたい場合は後述する`TypedDict`を使用します。

```python
from typing import Literal

AllowedKeys = Literal["a", "b"]    # OK
AValues = Literal[1, 2, 3]         # OK
#BValues = Literal[3.14]           # error: Parameter 2 of Literal[...] cannot be of type "float"
CValues = Literal[None]            # OK
AllowedValues = AValues | CValues

d: dict[AllowedKeys, AllowedValues] = {"a": 1, "b": None}  # OK
```

[TypedDict](https://docs.python.org/ja/3/library/typing.html#typing.TypedDict)を継承したクラスを定義することで、キー毎にデータ型を指定することができます。**デフォルトでは全てのキーは必須**となります。

```python
from typing import TypedDict

class Sample(TypedDict):
    a: int
    b: float

d: Sample = {"a": 1, "b": 3.14}  # OK
d: Sample = {"a": 1}             # error: Missing key "b" for TypedDict "Sample"
d: Sample = {"a": []}            # error: Incompatible types (expression has type "None", TypedDict item "b" has type "float")
```

クラス継承の引数に`total=False`を追加すると、**全てのキーは任意**となります。
```python
class Sample2(TypedDict, total=False):
    a: int
    b: float

d: Sample2 = {"a": 1, "b": 3.14}  # OK
d: Sample2 = {"a": 1}             # OK
d: Sample2 = {}                   # OK
```

必須と任意を混在させるには、[Required](https://docs.python.org/ja/3/library/typing.html#typing.Required)と[NotRequired](https://docs.python.org/ja/3/library/typing.html#typing.NotRequired)を使用します。`a`と`b`を必須とし、`c`のみ任意とする場合は以下のように定義します。

```python
from typing import NotRequired, Required, TypedDict

class Sample3(TypedDict):
    a: int
    b: float
    c: NotRequired[list]

d: Sample3 = {"a": 1, "b": 3.14, "c": []}  # OK
d: Sample3 = {"a": 1, "b": 3.14}           # OK
d: Sample3 = {"a": 1}                      # error: Missing key "b" for TypedDict "Sample3"
```

この`Sample3`クラスは、以下のように定義しても同じです。
「必須キーと任意キーのどちらが多いか」によって使い分けても良いでしょう。
```python
class Sample3(TypedDict, total=False):
    a: Required[int]
    b: Required[float]
    c: list
```

# まとめ
本記事では、辞書を活用するためのテクニックを10個紹介させていただきました。

初心者のみならず、辞書を使い慣れた中級者の方にとっても、新たな発見が1つでもありましたら幸いです。