defaultdictでコードをスッキリさせる

こんにちは、@tokioxxxjpnです。
フィグニー株式会社という会社のメンバーとして、企業のシステムのリプレイス、DXなどのプロジェクトに多く携わっています。
そういった業務の目線でみなさんと経験知見を共有しあえたらと思っています。

# defaultdictについて。

本記事ではdefaultdictについて紹介します。
基本的な使い方から、実務目線での利用例をお話させていただきます。

### なぜdefaultdict？

冒頭でも話しましたが、筆者は企業の業務効率化やデータの有効活用のためのシステム開発に携わることが多いのですが、大体の場合企業のデータは非常に雑多で入り組んでいます。

なのでそれらのデータをプログラム内で使いやすくするために整形するという作業がほぼ必ず発生します。

pythonであればlistやdictを駆使すれば基本的には事足りるのですが、pythonにはさらに一歩踏み込んだ、かゆいところに手が届くようなライブラリが標準で組み込まれており、その一つがdefaultdictです。


# defaultdictの基本的な使い方

```python
from collections import defaultdict

d_dict = defaultdict(int)
```

インポートして初期化するだけで準備はOKです。
引数には初期化時に実行したい関数を記述します。

まずはリストの中身を集計します。

```python
foods = [
    'curry', 'ramen', 'tonkatsu', 'ramen', 'sushi', 'curry', 'soba', 'curry', 
    'ramen', 'tenpra', 'udon', 'ramen', 'yakiniku', 'tonkatsu', 'soba' 
]

d_dict = defaultdict(int)

for food in foods:
    d_dict[food] += 1

print(d_dict)

# defaultdict(<class 'int'>, {'curry': 3, 'ramen': 4, 'tonkatsu': 2, 'sushi': 1, 'soba': 2, 'tenpra': 1, 'udon': 1, 'yakiniku': 1})
```

同じことを普通のdictで行うと、、、

```python
normal_dict = {}

for food in foods:
    normal_dict += 1
    
# 普通のdictだとキーエラーになる
# Traceback (most recent call last):
# KeyError: 'curry'
```

if文でキーの存在確認が必要になります

```python
for food in foods:
    if food in normal_dict:
        normal_dict[food] += 1
    else:
        normal_dict[food] = 1   
```

このように普通のdictとの大きな違いは**キーの存在確認が不要**ということです。

余談ですがリストなどイテラブルの要素の集計であれば同じくPython標準ライブラリcollectionsのCounterクラスのほうが適しています。

```python
from collections import Counter

foods = [
    'curry', 'ramen', 'tonkatsu', 'ramen', 'sushi', 'curry', 'soba', 'curry', 
    'ramen', 'tenpra', 'udon', 'ramen', 'yakiniku', 'tonkatsu', 'soba' 
]

food_counter = Counter(foods)

print(food_counter)

# Counter({'ramen': 4, 'curry': 3, 'tonkatsu': 2, 'soba': 2, 'sushi': 1, 'tenpra': 1, 'udon': 1, 'yakiniku': 1})
```

# 実際に使ってみる

defaultdictのvalueの初期値はintだけでなくlistやdictも使えますし、defaultdictを入れ子にすることもできます。（複雑にしすぎるとdefaultdictの利点が活かせなくなりますが。）
lambda(無名関数)を記述して変則的な処理をすることも可能です。

```python
# 初期値が0ではなく1となる
d_dict_lambda = defaultdict(lambda: 1)

for food in foods:
    d_dict_lambda[food] += 1

print(d_dict_lambda)
# defaultdict(<function <lambda> at 0x1042504a0>, {'curry': 4, 'ramen': 5, 'tonkatsu': 3, 'sushi': 2, 'soba': 3, 'tenpra': 2, 'udon': 2, 'yakiniku': 2})
```

### 実務での利用例

実務で扱うデータ構造は複雑なことが多くこれらを必要に応じて整形していくと思ったよりコード量が多くなりがちです。
そういった状況でdefaultdictを用いることで可読性を上げ、先述のキーエラーなどを起こすリスクを減らすことができます。

```python
students_exam = {
    'taro': {'math': 70, 'japanese': 80, 'science': 40}, 
    'hanako': {'math': 50, 'science': 90, 'english': 50}, 
    'john': {'english': 90, 'music': 70, 'japanese': 50}, 
    'mika': {'social': 50, 'math': 60, 'science': 90}
}
```
生徒ごとのテストの点数をまとめたdictがあったとします。
これらを教科ごとに点数をリストにまとめたり教科ごとに生徒の点数をまとめる形でデータを作ります。

```python
subject_scores_l = defaultdict(list)

for results in students_exam.values():
    for subject, score in results.items():
        subject_scores_l[subject].append(score)

print(subject_scores_l)

# defaultdict(<class 'list'>, {'math': [70, 50, 60], 'japanese': [80, 50], 'science': [40, 90, 90], 'english': [50, 90], 'music': [70], 'social': [50]})

subject_scores_d = defaultdict(lambda : defaultdict(int))

for name, results in students_exam.items():
    for subject, score in results.items():
        subject_scores_d[subject][name] = score

print(subject_scores_d)

# defaultdict(<function <lambda> at 0x104215760>, {'math': defaultdict(<class 'int'>, {'taro': 70, 'hanako': 50, 'mika': 60}), 'japanese': defaultdict(<class 'int'>, {'taro': 80, 'john': 50}), 'science': defaultdict(<class 'int'>, {'taro': 40, 'hanako': 90, 'mika': 90}), 'english': defaultdict(<class 'int'>, {'hanako': 50, 'john': 90}), 'music': defaultdict(<class 'int'>, {'john': 70}), 'social': defaultdict(<class 'int'>, {'mika': 50})})
```

かなりシンプルな例ではありますが、普通のdictを用いて多重ループの中でif文のキーの存在確認をするとネストも深くなり可読性が著しく下がります。
defaultdictを有効に使うことでコード量が減り、「このdictの中身なんだっけ」という確認もしやすくなります。
変数名にこだわるなども同様ですが、実際の開発現場ではそういった配慮がとても大切になります。


# リンク
[公式ドキュメント](https://docs.python.org/ja/3.12/library/collections.html#collections.defaultdict:~:text=%E3%82%82%E7%B0%A1%E5%8D%98%E3%81%A7%E3%81%99%E3%80%82-,defaultdict%20%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88,-%C2%B6)

# 終わりに
弊社は業務系のプロジェクトだけでなくxR、ゲームなどさまざまな分野で活躍しております。

一緒に働く仲間も随時募集中です！！弊社HPをぜひご覧ください。

https://www.wantedly.com/companies/company_1847744/projects

![フィグニー 採用.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1923419/ec0390b1-ee8a-f41b-5017-8819752ade86.jpeg)