Python | データ構造強化:collections.defaultdict

Python Python
スポンサーリンク

概要(defaultdict は「未登録キーに自動で初期値を入れる」辞書)

collections.defaultdict は、存在しないキーにアクセスした瞬間に「初期値」を自動作成してくれる辞書です。通常の dict では KeyError になる場面でも、defaultdict なら事前チェックなしで c[key].append(…) や c[key] += 1 が書けます。頻度カウント、グループ化、ネスト構造の作成が短く、安全に書けるのが最大の利点です。

from collections import defaultdict

# 出現回数のカウント(事前チェック不要)
cnt = defaultdict(int)
for w in ["coffee", "tea", "coffee"]:
    cnt[w] += 1
print(cnt)  # defaultdict(<class 'int'>, {'coffee': 2, 'tea': 1})
Python

基本の使い方(ここが重要)

default_factory に「初期値を作る関数」を渡す

defaultdict の第1引数 default_factory は「未登録キーに対してどんな初期値を作るか」を決めます。int は 0、list は []、set は set() を返します。未登録キーにアクセスした瞬間、辞書に新しいキーが作られ、その値が初期化されます。

from collections import defaultdict

# リストでグループ化
groups = defaultdict(list)
for user, team in [("hanako", "A"), ("taro", "B"), ("mika", "A")]:
    groups[team].append(user)
print(groups)  # {'A': ['hanako', 'mika'], 'B': ['taro']}
Python

直接加算・直接 append がそのまま書ける

存在確認や初期化の if を省けます。dict では必要な「キーの有無チェック」が、defaultdict なら不要です。

from collections import defaultdict

# 集計
scores = defaultdict(int)
scores["coffee"] += 3     # 未登録 → 0 を作ってから +3
scores["tea"] += 1
print(scores["sugar"])    # 未登録 → 0 が返る(ついでにキーもできる)
Python

default_factory の深掘り(柔軟な初期化)

組み込み型(int, list, set, dict)と関数・lambda

よく使うのは int、list、set。さらに「関数」や「lambda」を渡すと、複雑な初期値も生成できます。

from collections import defaultdict

# 集計+重複排除
by_ext = defaultdict(set)
by_ext[".csv"].add("data1.csv")
by_ext[".csv"].add("data1.csv")  # set なので重複しない

# 独自初期値(辞書を入れ子に)
def make_inner():
    return {"sum": 0, "count": 0}
stats = defaultdict(make_inner)
stats["coffee"]["sum"] += 120
stats["coffee"]["count"] += 2
Python

ネストした defaultdict で入れ子構造を簡潔に作る

入れ子の構造はネスト defaultdict が便利です。深さごとの初期化を自動化できます。

from collections import defaultdict

Tree = lambda: defaultdict(Tree)
root = Tree()
root["A"]["B"]["C"] = 1
print(root)  # 未登録の枝が自動生成される
Python

dict.get と比較して理解する(設計の違い)

dict.get は「値を返すだけ」、defaultdict は「キーを追加する」

dict.get(key, default) は値を返すだけで、辞書にはキーが追加されません。defaultdict はアクセス時にキーと初期値が実体として追加されるため、その後の更新が自然に書けます。カウントやグループ化なら defaultdict が簡潔で安全です。

# dict.get との違い(get は辞書に追加しない)
d = {}
x = d.get("coffee", 0)  # 0 を返すだけ
# d["coffee"] は依然として未登録

# defaultdict ならアクセス時にキーができる
from collections import defaultdict
dd = defaultdict(int)
print(dd["coffee"])  # 0 を返し、辞書に 'coffee': 0 が追加される
Python

実務での使いどころ(重要ポイントの深掘り)

頻度カウント・グループ化・多段集計を最短で書く

カウントは int、グループ化は list、重複排除は set。多段集計はネスト。defaultdict を使えば「事前初期化の定型」が消え、ロジックが前面に出ます。

from collections import defaultdict

# 1: 頻度カウント
freq = defaultdict(int)
for word in "coffee tea tea sugar coffee coffee".split():
    freq[word] += 1

# 2: グループ化
group = defaultdict(list)
for user, team in [("hanako", "A"), ("taro", "B"), ("mika", "A")]:
    group[team].append(user)

# 3: 重複排除のグループ化
uniq = defaultdict(set)
for file in ["a.csv", "b.csv", "a.csv"]:
    uniq["csv"].add(file)
Python

既存 dict に戻したいときは dict() に渡す

外部APIや他ライブラリに渡す際に「普通の dict が欲しい」なら、dict(defaultdict_obj) で変換します。中身はそのままです。

from collections import defaultdict

g = defaultdict(list)
g["A"].append("hanako")
plain = dict(g)  # 普通の dict に変換
Python

注意点(ミュータブルの共有・メモリ・None不可)

ミュータブルな初期値は「キーごとに別インスタンス」

list や dict を初期値にするとき、各キーで別々のインスタンスが生成されます(安全)。ただし「一つのインスタンスを全キーで使い回す」ような設計は誤りになりがちなので避けます。

from collections import defaultdict

lists = defaultdict(list)
lists["A"].append(1)
lists["B"].append(2)
# A と B は別々のリスト(混ざらない)
Python

default_factory=None は禁止(KeyError が発生)

defaultdict を「普通の dict のように」使いたいなら、そもそも dict を使いましょう。default_factory を None にすると、未登録キーで KeyError が出ます。

from collections import defaultdict

# defaultdict(None) は未登録キーで初期化しない → KeyError
d = defaultdict(None)
# d["x"]  # KeyError
Python

無制限なキー追加に注意(メモリが増える)

アクセスするだけでキーが増えるため、意図せぬ入力(タイポなど)で辞書が肥大化することがあります。入力値のホワイトリストや検証を組み合わせると安全です。

例題で身につける(定番から一歩先まで)

例題1:CSV の特定列でグループ化して整形出力

import csv
from collections import defaultdict

def group_by_col(path: str, col: int) -> dict[str, list[list[str]]]:
    groups = defaultdict(list)
    with open(path, "r", encoding="utf-8", newline="") as f:
        for row in csv.reader(f):
            if len(row) > col:
                groups[row[col]].append(row)
    return dict(groups)  # 普通の dict にして返す

# 使い方例
# print(group_by_col("sales.csv", 0))
Python

例題2:拡張子ごとにファイル名を集める(重複排除)

from pathlib import Path
from collections import defaultdict

def files_by_ext(root: str) -> dict[str, set[str]]:
    by_ext = defaultdict(set)
    for p in Path(root).rglob("*"):
        if p.is_file():
            by_ext[p.suffix.lower()].add(p.name)
    return dict(by_ext)

# print(files_by_ext("downloads"))
Python

例題3:2段階の集計(カテゴリ別に数量合計)

from collections import defaultdict

def sum_by_category(rows: list[tuple[str, str, int]]) -> dict[str, dict[str, int]]:
    total = defaultdict(lambda: defaultdict(int))
    for cat, name, qty in rows:
        total[cat][name] += qty
    return {cat: dict(names) for cat, names in total.items()}

data = [("drink", "coffee", 2), ("drink", "tea", 1), ("snack", "cookie", 3)]
# print(sum_by_category(data))
Python

例題4:正規表現で単語抽出し、先頭文字でグループ化

import re
from collections import defaultdict

def group_by_initial(text: str) -> dict[str, list[str]]:
    groups = defaultdict(list)
    for w in re.findall(r"[A-Za-z]+", text):
        groups[w[0].lower()].append(w.lower())
    return dict(groups)

# print(group_by_initial("Coffee tea Sugar syrup"))
Python

まとめ

defaultdict は「未登録キーの初期化を自動化」することで、カウント、グループ化、ネスト構造の作成を圧倒的に短く、安全にします。default_factory に int・list・set・関数を渡して用途に合わせた初期値を用意し、c[key] += 1 や c[key].append(x) を事前チェックなしで書く。dict.get との違いは「アクセス時にキーが実体として追加される」点で、集計系では特に強みが出ます。ミュータブル初期値はキーごとに別インスタンス、default_factory=None は避ける、入力検証で無制限追加を防ぐ。この基本線を身につければ、初心者でも“辞書まわりの定型”を捨て、ロジックに集中した美しいコードが書けます。

タイトルとURLをコピーしました