defaultdict : 条件のない辞書の進化

Pythonには数多くの外部ライブラリがありますが、標準ライブラリだけを正しく理解しても、実務で十分に強力なコードを書くことができます。今回はその中でも collections.defaultdict を深く掘り下げていきます。

この記事を通して、単なる概念紹介を超えて、実際にいつ、なぜ、どのように defaultdict を使うべきかを明確に理解できるようになるでしょう。

collectionsシリーズの第一弾、Counterクラスについて興味がある方は、以前の記事を読むことをお勧めします。 Python標準ライブラリ ① - collections.Counter


1. 基本概念: defaultdictとは?

Tux with defaultdicts

defaultdictはPython標準ライブラリの collections モジュールに含まれる特別な辞書(dict)のサブクラスです。一般的な辞書では存在しないキーにアクセスすると KeyError が発生しますが、 defaultdict では 自動的にデフォルト値を生成する関数(factory function)を指定することができ、コードがずっとクリーンになり、エラーを防ぐことができます。


2. 基本的な使い方

from collections import defaultdict

d = defaultdict(int)
d['apple'] += 1
print(d)  # defaultdict(<class 'int'>, {'apple': 1})

ここで int() はデフォルト値として 0 を返します。存在しないキー 'apple' にアクセスすると、 KeyError なしで 0 が自動的に生成され、その後 +1 が実行されます。


3. 様々なデフォルト値の例

from collections import defaultdict

# デフォルト値: 0 (int)
counter = defaultdict(int)
counter['a'] += 1
print(counter)  # defaultdict(<class 'int'>, {'a': 1})

# デフォルト値: 空のリスト
group = defaultdict(list)
group['fruit'].append('apple')
group['fruit'].append('banana')
print(group)  # defaultdict(<class 'list'>, {'fruit': ['apple', 'banana']})

# デフォルト値: 空のセット
unique_tags = defaultdict(set)
unique_tags['tags'].add('python')
unique_tags['tags'].add('coding')
print(unique_tags)  # defaultdict(<class 'set'>, {'tags': {'python', 'coding'}})

# デフォルト値: カスタム初期値
fixed = defaultdict(lambda: 100)
print(fixed['unknown'])  # 100

4. 実例

1. 単語頻出数を数える

words = ['apple', 'banana', 'apple', 'orange', 'banana']
counter = defaultdict(int)

for word in words:
    counter[word] += 1

print(counter)
# defaultdict(<class 'int'>, {'apple': 2, 'banana': 2, 'orange': 1})

👉 Counter vs defaultdict
単語の頻出数を数えるには collections.Counter() がより特化しているため、統計やランキング分析が必要な場合は Counter を使用するのが望ましいです。しかし、累積カウントのような単純な合算には defaultdict(int) でも十分に簡潔に使用できます。

2. ログをグループ別に整理する

logs = [
    ('2024-01-01', 'INFO'),
    ('2024-01-01', 'ERROR'),
    ('2024-01-02', 'DEBUG'),
]

grouped = defaultdict(list)
for date, level in logs:
    grouped[date].append(level)

print(grouped)
# defaultdict(<class 'list'>, {'2024-01-01': ['INFO', 'ERROR'], '2024-01-02': ['DEBUG']})

3. 重複を除いたタグ整理

entries = [
    ('post1', 'python'),
    ('post1', 'coding'),
    ('post1', 'python'),
]

tags = defaultdict(set)
for post, tag in entries:
    tags[post].add(tag)

print(tags)
# defaultdict(<class 'set'>, {'post1': {'python', 'coding'}})

5. 注意事項

  • defaultdictは内部的にデフォルト値生成器を保存するため、 repr() で一般的な dict とは異なる形状になる可能性があります。
  • JSONシリアル化時に問題となることがあります。 dict(d) に変換してから処理するのが安全です。
  • [] にアクセスする時のみデフォルト値が生成されます。 get() でアクセスしても生成されません。
from collections import defaultdict

d = defaultdict(list)
print(d.get('missing'))  # None
print(d['missing'])      # []

6. いつ使うと良いか? - defaultdict が持つ決定的な利点 3つ

defaultdictdict + 条件文パターンを多く使用する状況で、 可読性、保守性、安全性 を同時に高めてくれます。特に次の3つの状況で「これは絶対にdefaultdictを使ったほうが良い!」と感じることになるでしょう。

6-1. 条件なしでカウント/累積する集計コード

from collections import defaultdict

# 一般的な dict
counts = {}
for item in items:
    if item not in counts:
        counts[item] = 0
    counts[item] += 1

# defaultdict
counts = defaultdict(int)
for item in items:
    counts[item] += 1

✔ 条件文が消えることでコードが簡潔になり、ミスの余地も減ります。
✔ 特にログ分析、単語カウントなど、大量データ処理に非常に適しています。

6-2. リスト/セットを累積する時に setdefault を代替

from collections import defaultdict

# 一般的な dict
posts = {}
for tag, post in data:
    if tag not in posts:
        posts[tag] = []
    posts[tag].append(post)

# defaultdict
posts = defaultdict(list)
for tag, post in data:
    posts[tag].append(post)

setdefault() よりも直感的で、ループ内でもすっきりしています。
データグルーピングに最適化された構造です。

6-3. ネストされた辞書の構成時に初期化を自動化

# 一般的な辞書
matrix = {}
if 'x' not in matrix:
    matrix['x'] = {}
matrix['x']['y'] = 10

# defaultdictでのネスト
matrix = defaultdict(lambda: defaultdict(int))  # キーがない時毎に内部にdefaultdict(int)が自動生成される
matrix['x']['y'] += 10

✔ ネストされたデータ構造を手軽に作成できるため、 多次元辞書の作業に非常に有利です。
✔ データマイニング、パース、ツリー構造の保存などによい強力な長所を発揮します。

lambda: defaultdict(int) は内部的にキーがない時毎に defaultdict(int) を返す構造で、辞書が自動的にネスト生成されます。


7. まとめ

collections.defaultdict は初心者には単純な dict の拡張版のように見えるかもしれませんが、使えば使うほど コードをより明確で安全にする構造的なツールであることを実感するでしょう。

  • KeyError を心配せずに辞書を使える。
  • 条件なしでデータをグループ化して累積できる。
  • ネストされた辞書を直感的に構成できる。
# defaultdictで安定した簡潔な処理の例
salaries = defaultdict(int)
for dept, amount in records:
    salaries[dept] += amount

1行のコードでエラー防止 + 可読性向上 + 保守性向上が実現できれば、
defaultdict は単なる便利機能ではなく、 Pythonらしい考え方の核心的なツールと言えるでしょう。

次に扱うテーマは pathlib です。
ファイル/ディレクトリを扱うコードを オブジェクト指向的に変えてくれるモダンな方法で、初心者から中級者まで誰でも便利に活用できます。 os.path に馴染みのある開発者の皆さんは「わあ!os.pathよりずっと簡単だ!」という感覚を感じることができるでしょう。次回もお楽しみに。