見出し画像

Python 3: Deep Dive (Part 3 - Dictionaries, Sets, JSON): 特化型ディクショナリ (セクション9-1/12)

  • Pythonの標準的なdictだけでなく、欠損キー対応や挿入順序維持、カウンタ機能など特化した辞書型が用意されている。

  • 特にdefaultdictを使うと、欠損キーが発生するたびに指定したファクトリ関数で自動的に初期値が挿入され、冗長なコードが簡潔になる。

  • defaultdictはintやlistなどの型コンストラクタや、非決定的な値を返す関数も指定でき、カウンタやグルーピングなど様々な状況で有用。

標準的なPythonのディクショナリ(辞書型)を使っていると、単なるキーと値の格納以上の機能が欲しくなることがよくあります。たとえば、挿入順序が保証されたり、欠損キーへの対応を自動化したり、カウンタとして機能したり、あるいは複数の辞書を結合して扱えるようにしたい場合があります。幸いなことに、Python標準ライブラリには、こうした特定の問題に対して洗練された解決策を提供する「特化型ディクショナリ」が複数用意されています。

本稿では、こうした専門的なディクショナリ型をいくつか紹介した上で、その中でも特に人気の高い defaultdict に注目してみます。

標準ディクショナリを超えて

Pythonの`dict`型は非常に多用途かつ効率的で、挿入・検索・削除は平均的にO(1)で行えます。Python 3.7以降では、ディクショナリは挿入順序を保持することも言語仕様として保証されました。

それでも、以下のような場面では標準のdictだけではやや不便に感じることがあります。

  • 欠損キー処理: 存在しないキーをアクセスすると`KeyError`が発生します。`get()`メソッドや条件分岐で対応することはできますが、コードが冗長になりがちです。

  • キー挿入順序の後方互換性: 新しいPythonでは標準で順序保持しますが、3.5以前に対応しなければならない場合、または順序関連の特別な機能が必要な場合、`OrderedDict`が有効です。

  • カウンタやマルチセットとしての利用: 頻度計測などで、辞書をカウンタとして使いたい場合、毎回初期化やインクリメント処理を書くのは面倒です。専用のカウンタ辞書があれば便利です。

  • 複数ディクショナリの結合表示: 複数の辞書を一度に参照したいが、コピーはしたくない場合、効率的なビューを与えるクラスが欲しくなります。

こうした課題に対応するため、`collections`モジュールには以下のような特化型ディクショナリがあります。

  1. defaultdict - 欠損キーに対して自動的にデフォルト値を提供する

  2. OrderedDict - 挿入順序を保証(古いPythonバージョン下で有用)し、さらにいくつかの順序操作関連のメソッドを提供

  3. Counter - キーをマルチセット要素のように扱い、出現回数の集計を容易にする

  4. ChainMap - 複数の辞書を効率的に連結して一つのマッピングとして扱うが、データのコピーはしない

  5. UserDict - `dict`を継承せずに独自の辞書型を作るための便利な基底クラス(特殊メソッドの問題を回避)

これらを使うことで、従来なら冗長になりがちな処理をスッキリとまとめることができます。

`defaultdict`の紹介

通常の辞書では、存在しないキーを参照すると`KeyError`が生じます。

d = {}
d['a']  # KeyError発生

これを回避するために`get()`を使うことができます。

value = d.get('a', 0)  # 'a'がなければ0を返す

シンプルなケースではこれで十分ですが、同じ辞書に対して同じデフォルト値を何度も指定する必要があったり、カウンタ的処理を簡潔に書きたい場合、`get()`を繰り返し使うコードは煩雑になります。

たとえば、文字列中の各文字の出現回数をカウントするとしましょう。

counts = {}
for c in sentence:
    counts[c] = counts.get(c, 0) + 1

これでも問題ありませんが、「0」を毎回指定する必要があり、他の箇所でも同じ処理をするなら、やはり面倒です。

ここで`defaultdict`が役立ちます。

`defaultdict`とは

`defaultdict`は`collections`モジュールにあり、`dict`を継承したクラスです。主な特徴は、存在しないキーが要求された際に自動的に「ファクトリ関数」を呼び出し、その戻り値でキーを初期化する点です。

以下は典型的な例です。

from collections import defaultdict

counts = defaultdict(lambda: 0)
for c in "able was I ere I saw elba":
    counts[c] += 1

これで、キーが存在しなかった場合、`counts[c]`は`lambda:0`を呼び出し、0を返してキーも挿入します。その後`+=1`でカウントを増やします。`get()`や条件分岐は不要です。コードが簡潔でわかりやすくなります。

また、`defaultdict`は`dict`のサブクラスなので、すべての辞書操作が可能です(`items()`や`keys()`など)。

複雑なデフォルト値

ファクトリ関数は単に0や定数を返すだけでなく、もっと複雑なオブジェクトを返せます。よく使われるパターンとして、`int`、`list`、`str`などのコンストラクタを直接指定する方法があります。

d = defaultdict(int)   # デフォルトは0
d = defaultdict(list)  # デフォルトは[]
d = defaultdict(str)   # デフォルトは""

例えば`defaultdict(list)`は、存在しないキーに対して自動的に空リストを割り当てるので、グルーピング処理が簡単になります。

people = {
    'john': {'age': 20, 'eye_color': 'blue'},
    'jack': {'age': 25, 'eye_color': 'brown'},
    'jill': {'age': 22, 'eye_color': 'blue'},
    'eric': {'age': 35},
    'michael': {'age': 27}
}

eye_colors = defaultdict(list)
for person, details in people.items():
    color = details.get('eye_color', 'unknown')
    eye_colors[color].append(person)

print(eye_colors)
# defaultdict(<class 'list'>, {'blue': ['john', 'jill'], 'brown': ['jack'], 'unknown': ['eric', 'michael']})

ここでは、`eye_colors[color]`を呼ぶたびに、そのキーがなければ新たに空リストが作られ、`append()`で人名を追加するだけです。

非決定的なデフォルト値

ファクトリ関数は常に同じ値を返す必要もありません。APIコールやデータベースアクセス、現在時刻の取得など、その時々で異なる値を返してもかまいません。デフォルト値を要求するたびに呼び出されるので、その時点で新しい値が得られます。

from collections import defaultdict
from datetime import datetime

d = defaultdict(lambda: datetime.utcnow())
print(d['first_call'])  # 現在時刻
print(d['second_call']) # 実行時点での別の時刻

このように、`defaultdict`は動的かつ文脈依存な初期値を得るためにも利用できます。


ここまでのまとめ

この記事では、Pythonの特化型ディクショナリを紹介し、その中でも`defaultdict`に注目しました。`defaultdict`を用いることで、欠損キーへの対応が容易になり、`get()`や条件分岐を多用する冗長なコードを簡潔にできます。

次回は`OrderedDict`や`Counter`、`ChainMap`、`UserDict`など、他の特化型ディクショナリについても掘り下げます。それぞれ挿入順序維持、カウンタ機能、複数辞書の統合ビュー、独自辞書型定義といった独自の利点を持っています。

当面は、`defaultdict`をぜひ活用してみてください。欠損キー対策に毎回同じ定型文を書く必要がなくなり、コードがより明快でエレガントになるはずです。


次回予告:
`OrderedDict`に深く踏み込み、通常の辞書との比較や、さらに高度な利用シナリオを解説します。お楽しみに!


「超本当にドラゴン」へ

いいなと思ったら応援しよう!