【Python】collections.defaultdictで初期値付き辞書を扱う

Python

こんにちは、かつコーチです。

Pythonで辞書にデータを追加していく処理を書いていると、KeyErrorに何度も遭遇した経験はありませんか。

「キーが存在するかどうかを毎回チェックするのが面倒」

そんな悩みを解決してくれるのがcollections.defaultdictです。

この記事では、通常のdictKeyErrorが起きる場面を確認したうえで、defaultdictでの解決方法、グルーピング処理の実例まで解説します。

読み終える頃には、リストのグルーピング処理をシンプルに書けるようになります。

通常のdictでKeyErrorが起きる場面

存在しないキーへの追加でエラーになる

まずは、通常の辞書(dict)でグルーピング処理を書いたときに何が起きるか見てみましょう。

学生を学年ごとにグループ分けするコードを書きます。

students = [
    ("1年", "田中"),
    ("2年", "佐藤"),
    ("1年", "鈴木"),
    ("3年", "高橋"),
]

groups = {}

for grade, name in students:
    groups[grade].append(name)

print(groups)

実行すると、次のエラーが発生します。

KeyError: '1年'

groups[grade]にアクセスした時点で、gradeというキーがまだ辞書に存在しないため、KeyErrorが発生してしまいます。

if文でキーの存在をチェックする対処法

通常のdictでこの問題を回避するには、キーが存在するかどうかをif文でチェックする必要があります。

students = [
    ("1年", "田中"),
    ("2年", "佐藤"),
    ("1年", "鈴木"),
    ("3年", "高橋"),
]

groups = {}

for grade, name in students:
    if grade not in groups:
        groups[grade] = []
    groups[grade].append(name)

print(groups)

実行結果は次の通りです。

{'1年': ['田中', '鈴木'], '2年': ['佐藤'], '3年': ['高橋']}

これでエラーは解消しますが、毎回if文を書くのは手間ですし、コードも読みにくくなります。

defaultdictで初期値を自動化する

defaultdictは、collectionsモジュールに含まれる、存在しないキーにアクセスしたときに自動で初期値を作ってくれる辞書クラスです。

作成時に、初期値を生成する関数(listintなど)を渡します。

from collections import defaultdict

students = [
    ("1年", "田中"),
    ("2年", "佐藤"),
    ("1年", "鈴木"),
    ("3年", "高橋"),
]

groups = defaultdict(list)

for grade, name in students:
    groups[grade].append(name)

print(groups)

実行結果は次の通りです。

defaultdict(<class 'list'>, {'1年': ['田中', '鈴木'], '2年': ['佐藤'], '3年': ['高橋']})

defaultdict(list)と指定することで、存在しないキーにアクセスした瞬間に自動で空のリスト[]が作られます。

そのため、if文によるチェックが一切不要になりました。

よくあるつまずきポイント・エラー対処

通常の辞書に変換せずにそのまま返して混乱する

これは筆者が実際にAPIのレスポンス処理を書いていたときに遭遇したつまずきです。

defaultdictの結果をそのまま関数の戻り値として使ったところ、呼び出し先で予期しない挙動が起きました。

❌ Before(defaultdictをそのまま返す書き方)

from collections import defaultdict

def group_students(students):
    groups = defaultdict(list)
    for grade, name in students:
        groups[grade].append(name)
    return groups

students = [("1年", "田中"), ("2年", "佐藤")]
result = group_students(students)

print(result["3年"])
print(result)

実行結果は次の通りです。

[]
defaultdict(<class 'list'>, {'1年': ['田中'], '2年': ['佐藤'], '3年': []})

result["3年"]と存在しないキーを参照しただけなのに、resultの中身に'3年': []が勝手に追加されてしまいました。

筆者はこの挙動を知らず、後続処理で辞書のキー一覧をチェックした際に「存在しないはずのキーが増えている」と原因調査に時間を使ってしまいました。

これはdefaultdictが「アクセスされた時点で初期値を作る」という仕様のためです。

✅ After(通常のdictに変換してから返す書き方)

外部に結果を渡す場合は、dict()で通常の辞書に変換してから返すのが安全です。

from collections import defaultdict

def group_students(students):
    groups = defaultdict(list)
    for grade, name in students:
        groups[grade].append(name)
    return dict(groups)

students = [("1年", "田中"), ("2年", "佐藤")]
result = group_students(students)

print(result.get("3年", "該当なし"))
print(result)

実行結果は次の通りです。

該当なし
{'1年': ['田中'], '2年': ['佐藤']}

dict(groups)で通常の辞書に変換すれば、存在しないキーへのアクセスで自動的にキーが増える心配がなくなります。

defaultdictは集計処理の途中では便利ですが、他の関数に結果を渡す前には通常のdictに変換する習慣をつけると安心です。

まとめ

この記事のポイント

  • 通常のdictは、存在しないキーへの追加でKeyErrorが発生する
  • defaultdictは、存在しないキーへのアクセス時に自動で初期値を作ってくれる
  • defaultdict(list)はグルーピング処理でよく使うパターン
  • defaultdictは存在しないキーへのアクセスだけでキーが増えてしまう
  • 外部に結果を渡す際はdict()で通常の辞書に変換すると安全

次に読むべき記事

  • collections.Counterで要素の出現回数を数える
  • 辞書内包表記・セット内包表記の書き方
  • zip()で複数のリストをまとめて処理する

タグ: Python, 中級者向け, 標準ライブラリ

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