
Python 3: Deep Dive (Part 3 - Dictionaries, Sets, JSON): OrderedDict (セクション9-2/12)
Python 3.6+ではdictが挿入順を保持するため、単純な順序管理ならOrderedDictは不要になりつつある。
Counterクラスは要素の頻度計測や最頻出要素の抽出、カウンタ同士の加減演算を容易にする。
特殊な順序操作(先頭要素のポップ、順序依存比較など)や高度な頻度解析が必要な場合、それぞれOrderedDictやCounterを使うとコードが簡潔で効率的になる。
Pythonは進化するにつれ、特化型辞書と組み込み型辞書の差異はかつてほど明確ではなくなってきました。Python 3.6以前では、キーの挿入順を確実に保持したい場合、`OrderedDict`を用いる必要がありました。しかし現在(3.7以降正式、3.6では実装依存で)、標準の辞書も挿入順を保持するため、多くの場面で`OrderedDict`は不要となっています。
さらに、要素の頻度を扱う、いわゆる「カウンティング」のような日常的処理も、従来の辞書や`defaultdict(int)`を使うと冗長になりがちです。ここで`Counter`クラスが登場し、アイテム数え上げ、最頻出要素の抽出、カウンタ同士の加減演算などを手軽に実行できるようにします。
本稿では、`OrderedDict`がかつて担っていた役割が現行バージョンのdictでどのように補えるかを検証し、その後、日々のカウント処理を一変させる`Counter`クラスを詳しく見ていきます。
OrderedDict と Python 3.6+ の Dict の比較
背景:
Python 3.6より前、組み込みのdictはキー順序を保証していませんでした。`OrderedDict`は、挿入順序を確実に保持することで、キー順依存の操作(最初または最後の要素ポップ、キーを先頭または末尾に移動、逆順イテレーションなど)を可能にしていました。
OrderedDictが提供する主な追加機能:
要素の先頭/末尾ポップ:
`OrderedDict`の`popitem(last=True/False)`で、最初または最後の挿入要素を取り出せます。
標準dictでは`popitem()`で末尾要素は容易に取れますが、先頭要素をポップするには`next(iter(d))`で最初のキーを取り、`pop`で個別に処理する必要があります。キーを先頭または末尾へ移動:
`OrderedDict`の`move_to_end(key, last=True)`で任意キーを先頭/末尾へ移せます。
通常のdictで同等機能を再現するには、キーをポップ&再挿入したり、複雑な手順を踏む必要があり、特に先頭への移動は手間がかかります。
性能とメンテナンス性:
`OrderedDict`の生成は、標準dictよりも若干遅く、順序管理のオーバーヘッドがあります。
Python 3.7+のdictは挿入順保持が組み込みで、「ほぼ無料」で提供されるため、効率面で有利です。
結論:
単なる挿入順保持が目的で、Python 3.7+(または3.6で実装依存挙動を許容)を使用しているなら、通常のdictで十分です。逆イテレーション(3.8より前)、先頭要素ポップ、キー順依存の厳密比較など、特別な機能が必要なら依然として`OrderedDict`が有利。それ以外は、ちょっとした補助関数で標準dictでも対応可能です。
頻度計数を容易にするCounterクラス
`OrderedDict`が順序管理に特化している一方、`Counter`(`collections`より提供)は頻度計数に特化しています。文字数カウントや単語数カウントは日常的な作業ですが、通常のdictや`defaultdict(int)`ではコードが煩雑になりがちです。
`Counter`を使えば、要素の頻度計算が一行で完結します。
from collections import Counter
counts = Counter(text)
これで`text`中の全文字/全要素の出現数が集計されます。
Counterがもたらす利点:
自動的な0初期値: 存在しないキーにアクセスしても0を返すので、カウンタ操作が簡略化。
容易な初期化: イテラブルを渡すだけで頻度表が構築可能。
最頻出要素の取得: `counts.most_common(5)`で上位5要素とその頻度が即座に得られる。
カウンタ同士の算術: `+`, `-`, `&`, `|`演算子でカウンタ間の加算、減算、共通部分、合併(最大値)などが自在。
繰り返しイテレーション: `counts.elements()`は各キーをそのカウント数だけ繰り返すイテレータを返し、マルチセットのように扱える。
例:
from collections import Counter
sentence = "the quick brown fox jumps over the lazy dog"
c = Counter(sentence)
# 最も頻度が高い3文字
print(c.most_common(3))
# [(' ', 8), ('e', 3), ('o', 4)] 例えばスペースが最頻出!
更新と減算:
複数のデータソース(例:販売履歴と返品履歴)があり、それらを統合・補正したい場合も`Counter`が有効です。
sales = Counter(['battery', 'cable', 'battery', 'mouse'])
refunds = Counter(['battery', 'mouse'])
net = sales - refunds
print(net.most_common()) # 調整後のトップアイテムがすぐ分かる
数学的操作:
`+`: カウンタ同士を足し合わせる(最大値は和)。
`-`: 一方を他方から引く(0や負値は除外)。
`&`: キーごとに最小値を採用(共通部分)。
`|`: キーごとに最大値を採用(合併的な最大側)。
Counter vs. 通常のdictの使い分け
頻度集計には`Counter`が強力です。単回の小規模計数なら通常のdictでも十分ですが、継続的な上位要素抽出やカウンタ合算・減算が頻繁なら`Counter`がコード量やバグリスクを大幅低減します。
まとめ:
Python 3.6+でdictが挿入順を保持するようになり、`OrderedDict`の必要性は減少。
ただし、先頭要素ポップ、逆イテレーション(3.8より前)、順序依存の平等比較など特殊ケースは`OrderedDict`が有用なまま。
頻度計数や上位N要素抽出、カウンタ演算に関しては`Counter`が圧倒的に便利で、日常的な処理を簡素化します。
現代のPythonでは、普段は通常のdictを使い、特別な順序操作が必要なら`OrderedDict`を、カウント処理を伴う場合は`Counter`を利用するのが最適解といえます。より少ないコードでより多くを実現し、辞書操作をかつてないほどシンプルかつパワフルに行いましょう。