Pythonのプログラムを記述する際、データの管理に辞書(dict)型を利用することは非常に一般的です。
しかし、標準の辞書を使用していると、存在しないキーにアクセスした際に発生するKeyErrorの処理に煩わしさを感じる場面が多々あります。
特に、データの集計やグルーピングを行う際、事前にキーの存在を確認して初期値を設定するコードは冗長になりがちです。
こうした課題をスマートに解決してくれるのが、Pythonの標準ライブラリ「collections」モジュールに含まれるdefaultdictです。
defaultdictを活用することで、コードの可読性を飛躍的に向上させ、バグの混入を防ぐことができます。
本記事では、defaultdictの基本的な仕組みから、実務で役立つ具体的な活用シーン、さらには標準の辞書メソッドとの使い分けまでを詳しく解説します。
標準の辞書型が抱える課題と解決策
Pythonの標準的な辞書型(dict)は非常に強力ですが、新しいキーに値を代入したり、既存の値を更新したりする際に注意が必要です。
KeyErrorとその回避策
標準の辞書では、存在しないキーを参照しようとするとプログラムが停止してしまいます。
例えば、単語の出現頻度をカウントするコードを考えてみましょう。
# 標準の辞書を使用した場合
counts = {}
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
for word in words:
# キーが存在しないとKeyErrorになるため、チェックが必要
if word in counts:
counts[word] += 1
else:
counts[word] = 1
print(counts)
{'apple': 3, 'banana': 2, 'cherry': 1}
この「キーが存在するかどうかを確認する」というステップは、頻繁に登場するパターンですが、コードの本質的なロジックを埋もれさせてしまう原因にもなります。
get()メソッドやsetdefault()による対応
標準辞書には、これを回避するためのget()メソッドやsetdefault()メソッドも用意されています。
# getメソッドを使用した場合
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
# setdefaultメソッドを使用した場合
groups = {}
# リストへの追加などの場合
groups.setdefault("fruits", []).append("apple")
これらのメソッドも便利ですが、ループの中で何度も呼び出すと記述が複雑になり、パフォーマンス面でもわずかなオーバーヘッドが生じる場合があります。
そこで登場するのが、初期値を自動生成するdefaultdictです。
defaultdictの基本概念と使い方
defaultdictは、辞書のサブクラスであり、「存在しないキーにアクセスした際に、自動的にデフォルト値を生成する」という特性を持っています。
インポートと基本形
defaultdictを使用するには、collectionsモジュールからインポートします。
from collections import defaultdict
# intをデフォルトファクトリとして指定
d = defaultdict(int)
ここで引数に渡しているintは、「デフォルトファクトリ(default_factory)」と呼ばれます。
キーが存在しない状態でアクセスが発生すると、この関数(またはクラス)が呼び出され、その戻り値が新しい値として辞書に格納されます。
なぜ「int」で0がセットされるのか
Pythonにおいて、int()を引数なしで呼び出すと0が返されます。
同様に、list()は空のリスト[]を、set()は空の集合を返します。
defaultdictはこの仕組みを利用して、初期値を設定します。
| 指定する型 | 初期値 | 主な用途 |
|---|---|---|
int | 0 | 数値のカウント、合計の算出 |
list | [] | 要素のグルーピング |
set | set() | 重複を除いた要素の収集 |
dict | {} | 入れ子(ネスト)構造の辞書作成 |
実戦的な活用シーン
ここからは、具体的な業務やデータ処理でよく遭遇するパターンを紹介します。
1. 要素のカウント(int)
最もポピュラーな使い方は、データの頻度集計です。
先ほどの単語カウントをdefaultdictで書き換えると、驚くほどスッキリします。
from collections import defaultdict
words = ["python", "java", "python", "cpp", "java", "python"]
counter = defaultdict(int)
for word in words:
# キーの存在チェックが不要。存在しなければ自動的に0がセットされる
counter[word] += 1
print(dict(counter))
{'python': 3, 'java': 2, 'cpp': 1}
if文による分岐が消え、コードの意図が明確になりました。
最後にdict()でキャストしているのは、出力結果を通常の辞書形式で見やすくするためですが、そのまま利用し続けても問題ありません。
2. データのグルーピング(list)
特定の属性ごとにデータをまとめたい場合、listをデフォルト値に設定するのが最適です。
from collections import defaultdict
data = [
("Electronics", "Laptop"),
("Electronics", "Smartphone"),
("Books", "Novel"),
("Electronics", "Tablet"),
("Books", "Textbook")
]
# リストを初期値にする
category_map = defaultdict(list)
for category, item in data:
category_map[category].append(item)
for category, items in category_map.items():
print(f"{category}: {items}")
Electronics: ['Laptop', 'Smartphone', 'Tablet']
Books: ['Novel', 'Textbook']
通常、辞書でリストを扱う場合は「キーがなければ空リストを作成して追加する」という2ステップが必要ですが、defaultdictならappend()を直接呼び出すだけで済みます。
3. ユニークな値の収集(set)
重複を排除しながらグルーピングしたい場合は、set(集合型)を利用します。
from collections import defaultdict
user_actions = [
("user_1", "login"),
("user_2", "login"),
("user_1", "view_page"),
("user_1", "login"), # 重複
]
unique_actions = defaultdict(set)
for user, action in user_actions:
unique_actions[user].add(action)
print(unique_actions["user_1"])
{'view_page', 'login'}
ラムダ式を用いたカスタム初期値の設定
組み込みの型だけでなく、ラムダ式(lambda)を渡すことで、独自の初期値を設定することも可能です。
任意の定数で初期化する
例えば、デフォルトのスコアを「100」に設定したい場合は、次のように記述します。
from collections import defaultdict
# 常に100を返す関数をデフォルト値として設定
score_board = defaultdict(lambda: 100)
score_board["Alice"] = 150
print(f"Alice: {score_board['Alice']}")
print(f"Bob: {score_board['Bob']}") # 未登録のBobは100になる
Alice: 150
Bob: 100
複雑なオブジェクトで初期化する
特定のクラスのインスタンスを初期値にしたい場合も、同様の手法が使えます。
class UserProfile:
def __init__(self):
self.points = 0
self.status = "guest"
# インスタンスを生成する関数を渡す
user_db = defaultdict(UserProfile)
# アクセスした瞬間にUserProfileインスタンスが生成される
print(user_db["new_user"].status)
guest
入れ子(多次元)の辞書構造を構築する
defaultdictの強力な応用例として、多階層の辞書を簡単に作成する手法があります。
2段階のネスト
from collections import defaultdict
# 内部の辞書がまたdefaultdict(int)であるような構造
nested_dict = defaultdict(lambda: defaultdict(int))
nested_dict["Japan"]["Tokyo"] += 1400
nested_dict["Japan"]["Osaka"] += 880
print(nested_dict["Japan"]["Tokyo"])
このように、再帰的な定義を利用することで、深い階層のデータ構造もエラーなしで構築できます。
ただし、階層が深すぎるとコードの可読性が下がるため、適切な設計を心がけましょう。
defaultdict利用時の注意点
非常に便利なdefaultdictですが、利用にあたって知っておくべき「癖」があります。
参照しただけでキーが作成される
defaultdictの最大の注意点は、「存在しないキーを読み取ろうとしただけで、そのキーが辞書に追加される」という動作です。
from collections import defaultdict
d = defaultdict(int)
print(f"参照前: {len(d)}")
# 値を確認する(読み取り)
value = d["dummy_key"]
print(f"参照後: {len(d)}")
print(f"現在の内容: {dict(d)}")
参照前: 0
参照後: 1
現在の内容: {'dummy_key': 0}
単に値の有無をチェックしたいだけであれば、if key in d: のように通常の辞書と同じ確認方法をとるか、get()メソッドを併用する必要があります。
意図しないキーが増え続けると、メモリを圧迫する可能性があるため注意が必要です。
json.dumps()でのシリアライズ
defaultdictオブジェクトをそのまま json.dumps() に渡すと、型が標準の辞書ではないためエラー(TypeError)が発生することがあります。
JSON形式で出力したり保存したりする場合は、必ず dict(d) として標準の辞書型に変換しましょう。
パフォーマンスの観点
大量のデータを処理する場合、dict.setdefault() と defaultdict のどちらが優れているか気になるかもしれません。
結論から言えば、多くのケースでdefaultdictの方が高速です。
setdefault() は、呼び出すたびにデフォルト値のオブジェクトを引数として生成しますが、defaultdictは内部の __missing__ メソッドが呼び出された時のみ値を生成するため、無駄な計算が抑えられます。
型ヒントとdefaultdict
2026年現在のモダンなPython開発においては、静的解析を支える型ヒントの記述が欠かせません。
typingモジュールの型ヒントを利用する場合、以下のように記述します。
from collections import defaultdict
from typing import DefaultDict, List
# 型ヒントの指定:キーがstr、値がList[int]
data_map: DefaultDict[str, List[int]] = defaultdict(list)
data_map["scores"].append(95)
DefaultDict型を使用することで、IDEの補完機能も正しく働き、開発効率が向上します。
まとめ
Pythonのcollections.defaultdictは、冗長なエラーハンドリングを排除し、ロジックを簡潔に保つための非常に優れたツールです。
- KeyErrorを気にせず、直感的に辞書を操作できる
int,list,setなどの初期値を自動で割り当てられるlambdaを使った柔軟な初期値設定が可能- 大規模データの集計において、コードの可読性とパフォーマンスの両面でメリットがある
一方で、参照するだけでキーが追加されるという副作用には注意が必要です。
特性を正しく理解し、通常の辞書や Counter クラスなどと適切に使い分けることで、より堅牢で美しいPythonコードを記述できるようになります。
日常的なスクリプト作成から、大規模なデータ分析基盤の構築まで、あらゆる場面でこの defaultdict を活用してみてください。
