Pythonの開発において、辞書型 (dict) はデータの管理に欠かせない非常に便利なデータ構造です。
しかし、存在しないキーにアクセスした際に発生する KeyError の処理に苦労した経験を持つ方も多いのではないでしょうか。
このような課題を解決するために用意されているのが、標準ライブラリの collections モジュールに含まれる defaultdict です。
この記事では、 defaultdict の基本的な使い方から、実務で役立つ応用テクニックまで詳しく紹介します。
コードの可読性を高め、よりスマートなプログラミングを実現するための知識を身につけていきましょう。
Pythonの辞書操作におけるKeyErrorの課題
通常の辞書型 (dict) を使用する場合、新しいキーに値を代入したり、既存のキーを参照したりすることは非常に簡単です。
しかし、辞書に存在しないキーに対して値を更新しようとすると、Pythonは即座に KeyError を送出します。
例えば、リストの要素をカウントする処理を書く場合、事前にそのキーが存在するかどうかを確認する条件分岐が必要です。
# 通常の辞書でのカウント処理
data = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']
count_dict = {}
for item in data:
if item in count_dict:
count_dict[item] += 1
else:
count_dict[item] = 1
print(count_dict)
{'apple': 3, 'banana': 2, 'orange': 1}
このコードでは、 if item in count_dict というチェックが毎回実行されており、コードが少し冗長に感じられます。
dict.get() メソッドや dict.setdefault() メソッドを使うことで回避は可能ですが、それでも記述が複雑になる傾向があります。
そこで登場するのが、初期値を自動的に生成してくれる defaultdict です。
collections.defaultdictの基本概念
defaultdict は、Pythonの標準ライブラリである collections モジュールに定義されているクラスです。
最大の特徴は、辞書作成時に「初期値を生成する関数 (factory関数) 」を指定できる点にあります。
存在しないキーにアクセスした際、その関数が自動的に呼び出され、戻り値が新しい要素の初期値として設定されます。
これにより、キーの存在確認を行うことなく、いきなり値の操作を開始することが可能になります。
defaultdictのインポートと宣言
defaultdict を使用するには、まず collections モジュールからインポートする必要があります。
from collections import defaultdict
# int型を初期値にする(0がデフォルトになる)
d = defaultdict(int)
ここで引数に渡している int は、呼び出されると 0 を返す関数として機能します。
同様に、 list を渡せば空のリスト [] が、 set を渡せば空の集合 set() が初期値になります。
主要な初期値(default_factory)の使い方
defaultdict に指定する引数によって、どのようなデータ構造を効率的に構築できるかが決まります。
ここでは、実務でよく使われる代表的な3つのパターンを詳しく見ていきましょう。
1. 整数 (int) を初期値にする:頻度集計
データの個数を数える際、 int を指定するのが最も一般的です。
int() を引数なしで実行すると 0 が返るため、存在しないキーにアクセスすると自動的に 0 がセットされます。
from collections import defaultdict
words = ['python', 'java', 'python', 'cpp', 'java', 'python']
counter = defaultdict(int)
for word in words:
# キーが存在しなくてもいきなり加算できる
counter[word] += 1
print(dict(counter))
{'python': 3, 'java': 2, 'cpp': 1}
先ほどの dict を使った例に比べて、 if 文による分岐が消え、非常にスッキリとした記述になりました。
2. リスト (list) を初期値にする:グループ化
特定のキーに対して複数の要素をまとめたい場合、 list を指定します。
存在しないキーに対して .append() を直接呼び出せるため、データのグループ化に非常に便利です。
from collections import defaultdict
# (カテゴリ, 名前) のリスト
items = [('fruit', 'apple'), ('vegetable', 'carrot'), ('fruit', 'banana')]
grouped_data = defaultdict(list)
for category, name in items:
# 存在しないキーなら空のリストが作られ、そこにappendされる
grouped_data[category].append(name)
print(dict(grouped_data))
{'fruit': ['apple', 'banana'], 'vegetable': ['carrot']}
このように、データベースのレコードを特定の属性ごとに分類するような処理で威力を発揮します。
3. 集合 (set) を初期値にする:重複のない管理
リストと同様ですが、重複を許容したくない場合には set を指定します。
from collections import defaultdict
user_actions = [('user1', 'login'), ('user1', 'view'), ('user1', 'login')]
unique_actions = defaultdict(set)
for user, action in user_actions:
unique_actions[user].add(action)
print(dict(unique_actions))
{'user1': {'login', 'view'}}
login が重複して追加されても、 set の性質により自動的に一意な値のみが保持されます。
defaultdictと他メソッドの比較
辞書の初期値を扱う方法は defaultdict 以外にも存在します。
それぞれの特性を理解し、適切な場面で使い分けることが重要です。
| 手法 | 特徴 | 主な用途 |
|---|---|---|
dict[key] = value | 標準的な代入。キーがないとエラー。 | キーの存在が確定している場合。 |
dict.get(key, default) | 値を取得するだけ。辞書は更新されない。 | 一時的なデフォルト値が必要な場合。 |
dict.setdefault(key, default) | キーがなければ追加し、値を返す。 | 特定の箇所で一度だけ初期化したい場合。 |
defaultdict | クラス全体で初期値ルールを共有。 | 大量のデータ集計や複雑なグループ化。 |
setdefault() は便利なメソッドですが、ループ内で何度も呼び出すと、引数に指定したデフォルトオブジェクトが毎回生成されるため、パフォーマンスに影響を与える可能性があります。
一方で、 defaultdict は初期値の生成が必要な時だけ factory関数を呼び出すため、効率的です。
応用:ラムダ式 (lambda) を活用したカスタム初期値
int や list といった型だけでなく、任意の初期値を設定したい場合は lambda式 を利用します。
例えば、初期値を 0 ではなく 100 から開始したい場合や、特定の文字列をデフォルトにしたい場合に有効です。
from collections import defaultdict
# 初期値を100にする
score_dict = defaultdict(lambda: 100)
print(score_dict['new_player']) # 100が表示される
# 特定のメッセージをデフォルトにする
status_dict = defaultdict(lambda: "Unknown Status")
print(status_dict['process_a']) # "Unknown Status"が表示される
100
Unknown Status
このように、 default_factory には「引数を持たず、値を返すオブジェクト」であれば何でも指定できます。
入れ子構造の辞書をスマートに作成する
defaultdict を再帰的に利用することで、多層構造の辞書(ネストされた辞書)を簡単に作成することも可能です。
通常、2次元配列のような辞書を作るには、親のキーが存在するか確認してから子の辞書を作るという手順が必要です。
from collections import defaultdict
# 再帰的に自分自身を呼び出す関数を定義
def nested_dict():
return defaultdict(nested_dict)
matrix = nested_dict()
# キーの存在を気にせず深い階層に値を代入できる
matrix['2026年']['1月']['売上'] = 500000
print(matrix['2026年']['1月']['売上'])
500000
この手法を使うと、設定ファイルや階層構造を持つデータを扱う際に、 if 文の連鎖を排除できます。
defaultdictを使用する際の注意点
非常に便利な defaultdict ですが、利用にあたって注意すべき落とし穴も存在します。
もっとも注意すべきは、「存在しないキーを参照しただけで要素が作成されてしまう」という点です。
デバッグ中などに値を「確認」するつもりでアクセスすると、意図せず辞書のサイズが大きくなってしまうことがあります。
from collections import defaultdict
d = defaultdict(int)
# 参照しただけで 'test' キーが作られ、値 0 が格納される
print(d['test'])
# 辞書の中身を確認すると 'test' が存在する
print(dict(d))
0
{'test': 0}
意図しないキーの追加を防ぎたい場合は、参照する前に in 演算子で確認するか、最終的に通常の dict にキャストして運用することをお勧めします。
dict(d) と記述するだけで、 defaultdict の特殊な振る舞いを解除した標準的な辞書に変換できます。
まとめ
Pythonの defaultdict は、辞書操作における煩わしい初期化処理を自動化してくれる非常に強力なツールです。
集計処理には int 、データの蓄積には list 、そして特殊な要件には lambda を組み合わせることで、コードの可読性は劇的に向上します。
特に複雑なデータ構造を扱うプロジェクトでは、 KeyError の対策を個別に書くよりも、 defaultdict で一括管理するほうが保守性も高まります。
今回紹介したテクニックを活用して、Pythonらしい、簡潔でスマートなプログラムを記述してみてください。
日々のコーディングにおいて、 collections モジュールの便利さを実感できるはずです。
