辞書型(dict)の使い所のまとめ【Python】
PythonのdictやGo、C++のmapなど、多くのプログラミング言語には「辞書型(連想配列/ハッシュマップ)」と呼ばれるデータ構造が標準で用意されています。 この記事では、辞書型の基本的な使い方から、どのような場面でリスト(配列)よりも辞書型を使うべきかをPythonコードを使ってまとめてみました。辞書型の便利さを理解する助けになればと思います。
辞書型(dict)とは
辞書型とは、**キー(Key)と値(Value)のペアを格納するデータ構造です。 リスト(配列)が「0番目」「1番目」といったインデックス(数値)**で要素を管理するのに対し、辞書型は文字列や数値など、任意の「キー」を使ってデータにアクセスすることが可能です。

内部的には「ハッシュテーブル」という構造で管理されていて、データの量が増えても検索速度がほとんど落ちません。
宣言方法
Pythonの場合、辞書型の宣言には波括弧 {} を使うのが一般的です。また、dict() コンストラクタを使う方法もあります。
配列は [] を使って宣言しますが、辞書型は {} で宣言するのが異なります。
また、初期化時に値を設定する場合は、「キー:値」という形で宣言する必要があります。
# 空のdictを作成
x = dict()
y = {}
# 最初から要素の入ったdictを作成
z = {"a": 1, "b": 2}
# キーワード引数を使って作成することも可能(キーが文字列の場合に便利)
w = dict(name="Taro", age=20, country="Japan")
# -> {'name': 'Taro', 'age': 20, 'country': 'Japan'}要素の追加・更新
キーを指定して値を代入することで、新しいペアを追加したり、既存の値を上書きしたりできます。 また、update() メソッドを使うことで、別の辞書の内容をまとめてマージすることも可能です。
x = {}
# 要素を個別に追加
x['a'] = 1
x['b'] = 2
# 既存のキーの値を更新('a'の値は1から100に変わる)
x['a'] = 100
# 別の辞書の内容をまとめて追加・更新
x.update({'c': 3, 'd': 4})
# -> {'a': 100, 'b': 2, 'c': 3, 'd': 4}キーや値の取得
辞書の中身を取り出す方法はいくつかあります。
keys()を使うとキーを取得できます。また、values()を使うと値を取得できます。
items() は、キーと値をセットでループ処理する場合などに使います。
ちなみに、for e in data のように辞書だけを指定した場合、e にはキーが格納されます。
data = {"apple": 100, "banana": 200, "orange": 300}
# すべてのキーを取得
keys = data.keys()
# -> dict_keys(['apple', 'banana', 'orange'])
# すべての値を取得
values = data.values()
# -> dict_values([100, 200, 300])
# キーと値のペアをタプルで取得
items = data.items()
# -> dict_items([('apple', 100), ('banana', 200), ('orange', 300)])
# forループでの活用例
for key, value in data.items():
print(f"{key} の価格は {value} 円です")キーが含まれているか調べる
キーが辞書に含まれているかを調べるには、in 演算子を利用します。
in 演算子を使うことで、特定のキーが辞書に存在するかを判定できます。 リストの中から特定の要素を探す場合、リストの先頭から順に調べるためデータ量に比例して時間がかかりますが、辞書型の場合はデータ量が100万件あってもほぼ一瞬で判定できる(計算量が $O(1)$ )のが大きなメリットです。

注意:C++の std::map は $O(\log N)$ です。これはデータ構造の差です。また、C++でも std::unordered_map を使えば $O(1)$ です。
if "apple" in data:
print("appleは存在します")
else:
print("appleは登録されていません")Pythonの場合、配列でも if xxx in data という記述が可能ですが、辞書型と配列では処理量が全く異なることに注意が必要です。配列の場合は、$O(\text{要素数})$ となり、要素数が多くなると速度が低下します。

if文だけ見ると配列なのか辞書なのかわからないので、他人のコードが遅い場合にはここは注意して確認する必要があります。
配列から辞書への変換
例えば、name = ['taro', 'tom', 'hanako'] という名前のリストと、age = [20, 21, 19] という年齢のリストが別々に存在する場合を考えてみます。これらを「名前をキー、年齢を値」とする一つの辞書にまとめたい場合、Pythonの組み込み関数である zip() を活用するのが一般的です。
zip() 関数は、複数のリストの要素を先頭から順にペア(タプル)としてまとめてくれるため、これをそのまま dict() コンストラクタに渡すか、内包表記を使うことで簡単に変換することができます。
name = ['taro', 'tom', 'hanako']
age = [20, 21, 19]
# 方法1:dict()コンストラクタとzipを使う(シンプルで推奨される書き方)
# zipにより ('taro', 20), ('tom', 21)... というペアが作られ、辞書化されます
d = dict(zip(name, age))
# -> {'taro': 20, 'tom': 21, 'hanako': 19}
# 方法2:辞書内包表記を使う(値を加工しながら作成したい場合などに便利)
d_comp = {n: a for n, a in zip(name, age)}注意点:存在しないキーへのアクセス
辞書に存在しないキーにアクセスしようとすると、Pythonでは KeyError が発生し、プログラムが停止してしまいます。
この点が注意点になります。辞書にアクセスする場合は、if xx in で確認するか、後述する get メソッドを使う必要があります。
d = {"a": 1}
# print(d["b"])
# -> KeyError: 'b' が発生対処法1:getメソッドを使う
.get() メソッドを使うのが最も安全で一般的です。キーが存在しない場合は None(または指定したデフォルト値)を返してくれます。
また、キーが存在しなかった場合のデフォルト値を指定することも可能です。
# エラーにならず None が返る
val = d.get("b")
# キーがない場合のデフォルト値を指定(例:0)
val_with_default = d.get("b", 0) 対処法2:try-exceptを使う
なお、例外処理として明示的に記述する方法もあります。
try:
print(d["b"])
except KeyError:
print("キーが見つかりませんでした")defaultdictを使う
個人的には、collections.defaultdict を使うことをお勧めします。 これを使うと、辞書への初回のアクセス時にキーが存在しない場合、自動的に初期値を生成してセットしてくれます。「キーがあるか確認して、なければ初期化する」という定型コードを書かずに済みます。

Pythonの場合、キーに対応する値として、文字列や整数値など好きなものが入れられるのですが、defaultdict は、あらかじめ戻り値の型を宣言することで、存在しない場合の値に初期値を設定できるようにしたものと理解できます。
from collections import defaultdict
# intを指定すると、初期値が自動的に 0 になる(listなら空リスト、strなら空文字になる)
d = defaultdict(int)
# キー 'a' はまだないが、自動で0が作られ、それに1を足す処理になる
d['a'] += 1
d['b'] += 5
print(d)
# 出力: defaultdict(<class 'int'>, {'a': 1, 'b': 5})
print(d['c'])
# 出力: 0辞書型を使う実例
ここでは、実際にどのような場面で辞書型が役立つのか、具体的なユースケースを紹介します。
人名別で得票数を数える
投票用紙に書かれた名前と得票数をカウントしたい場合、辞書型は非常に強力です。 もしこれを配列で行おうとすると、「佐藤さん=ID 0, 鈴木さん=ID 1…」のように名前を数値IDに変換・管理するマッピングが必要になります。辞書型なら、名前(文字列)をそのままキーとして扱えるため、直感的かつシンプルに実装できます。
votes = ["佐藤", "鈴木", "佐藤", "田中", "鈴木", "佐藤"]
# defaultdictを使ったスマートな集計
result = defaultdict(int)
for name in votes:
# キーの存在確認が不要。単純に加算するだけでOK
result[name] += 1
print(dict(result))
# 出力: {'佐藤': 3, '鈴木': 2, '田中': 1}
このように、カテゴリごとの出現回数を数える「頻度分析」において、辞書型は定石と言えるデータ構造です。
配列だとメモリ不足になるような場合(疎なデータ)
データの範囲は広いけれど、実際に値が入っている箇所はごくわずかというケース(疎行列/Sparse Matrix)です。
例えば、巨大なゲームマップや科学計算において、インデックスの範囲が 0 から 1,000,000,000 まであるとします。 これを配列(リスト)で確保しようとすると、たとえ要素が5個しかなくても10億個分のメモリ領域([0, 0, ..., 0])を確保する必要があり、メモリ不足(Memory Error)になるか、無駄なメモリを大量に消費します。
辞書型であれば、「値が存在するインデックス」だけを保存すれば良いため、メモリを劇的に節約できます。

このテクニックは、競技プログラミングで頻繁に使われる定石ですし、実務でも有用です。
# 10億番目の要素だけに値を入れたい場合
# × リストの場合:
# huge_list = [0] * 1000000000 <-- ここで大量のメモリを消費
# huge_list[999999999] = 1
# ○ 辞書の場合:
# 必要なメモリは「1要素分」だけ
sparse_data = {}
sparse_data[999999999] = 1
print(sparse_data[999999999]) # -> 1複数要素の組み合わせ(タプルをキーにする)
Pythonの辞書型のキーには、数値や文字列だけでなく、変更不可(イミュータブル)な型であれば何でも使えます。そのため、リストはキーにできませんが、タプルはキーにすることができます。 例えば、グリッド上の座標 (x, y) ごとのデータを持ちたい場合や、複数の条件を組み合わせたキーを作りたい場合に便利です。
grid_data = {}
# (x, y) 座標をタプルとしてキーにする
grid_data[(10, 20)] = "Enemy"
grid_data[(5, 5)] = "Player"
# 直感的に座標データにアクセス可能
print(grid_data[(10, 20)])
# 出力: Enemy
これを多次元配列(リストのリスト)で実現しようとすると、grid[10][20] のように書けますが、事前に 11x21 のサイズの配列を確保しなければならず、前述の「疎なデータ」の問題に直面します。辞書型なら必要な座標の分だけデータを保持できます。
また、(文字列, 数値)といったタプルも利用できますので、配列と違って柔軟な設計が可能なのも特徴です。
複数要素の組み合わせ(文字列化)
データのシリアライズ(JSON化して保存、API通信など)を考慮する場合や、可読性のために、複数の変数を文字列として結合してキーにすることもあります。 JSONなどのフォーマットでは、辞書のキーは「文字列」であることが求められることが多いため、タプルキーが使えない場合の代替手段として知っておくと便利です。
x = 'abc'
y = 10
z = 40
# アンダースコアなどでつないで一意な文字列キーを作成
key = f"{x}_{y}_{z}"
# keyは "abc_10_40" となる
status_map = {}
status_map[key] = "OK"
# 復元する際は split で分解する
print(key.split('_'))
# -> ['abc', '10', '40']
Python内部だけで完結する処理ならタプルキーの方が高速で安全ですが、外部出力やログの見やすさを意識する場面では、この文字列化テクニックが使われることがあります。注意点は、文字列にした場合の浮動小数の取り扱いですが、逆に「小数点以下3桁が一致していれば同じとみなす」といったことがやりやすいです。
辞書を使う場合のまとめ
辞書型(dict)は、単なるデータの入れ物以上に、プログラムの効率を左右する重要なツールです。以下のような場面では、リストよりも辞書型の使用を検討しましょう。
- 高速な探索
keyを使って特定のデータを一発で見つけたい時(リストのような全探索 $O(n)$ が不要で、常に高速 $O(1)$)。 - 集計・カウント
名前、カテゴリ、IDごとの出現回数をカウントする時(ヒストグラム作成)。 - 疎なデータ(Sparse Data
定義域(インデックスの範囲)は広いが、実際の有効データ数が少ない時。メモリ効率が圧倒的に良い。 - ラベル付きデータの管理
インデックス番号ではなく、意味のある名前(IDや名称、属性名)でデータを管理し、コードの可読性を上げたい時。
適切に使い分けることで、コードが読みやすくなるだけでなく、パフォーマンスやメモリ効率も大幅に向上させることができます。

