Python WebAcademy Blog

Pythonのcollectionsとは?Counterやdefaultdictで毎日のコードが短くなる使い方を解説

|

Python標準ライブラリのcollectionsについて、出番の多いCounter・defaultdict・deque・namedtupleの4つをIT初心者向けにやさしく解説します。素のdictやlistで書いた場合との違い、実際に動かした実行結果、10万件で測った速度差、使い分けの目安まで順番に紹介します。

Pythonを書いていて、似たような数行を何度も打っている気がしませんか。

リストの中の単語を数える処理。辞書にキーがあるか確かめてから値を足す処理。

どちらもfor文とif文を組み合わせれば書けます。書けるのですが、毎回組み立てるのは地味に面倒です。

そういう定番の処理を、はじめから用意された入れ物で置き換えてくれるのが標準ライブラリのcollectionsです。Pythonに最初から入っているので、追加インストールは要りません。

この記事では、collectionsの中でも出番の多い4つを取り上げます。実際に動かした結果を並べながら、どんな場面で効いてくるのかを見ていきましょう。

collectionsには何が入っているのか

まずは全体像から確認します。collectionsは、辞書やリストやタプルを少しだけ賢くした入れ物を集めたモジュールです。

手元のPython 3.11で中身を表示してみました。

import collections

print([name for name in dir(collections) if not name.startswith("_")])

実行結果は次のとおりです。

['ChainMap', 'Counter', 'OrderedDict', 'UserDict', 'UserList', 'UserString', 'defaultdict', 'deque', 'namedtuple']

9つ並んでいます。ただ、学び始めの段階で覚えたいのは、このうち4つだけです。

この記事で扱うのはCounter、defaultdict、deque、namedtupleの4つになります。どれも、素のPythonで書くと数行かかる処理をぐっと縮めてくれる道具です。

数を数えるならCounter

いちばん出番が多いのはCounterかもしれません。何かの登場回数を数える処理は、練習でも実務でも驚くほどよく出てきます。

まずはcollectionsを使わずに書いてみます。単語のリストから、それぞれが何回出てきたかを数えるコードです。

words = ["python", "flask", "python", "django", "flask", "python"]

counts = {}
for w in words:
    if w in counts:
        counts[w] += 1
    else:
        counts[w] = 1

print(counts)

実行するとこうなります。

{'python': 3, 'flask': 2, 'django': 1}

きちんと動きます。ただ、キーがあるかどうかを確かめるif文が、少し邪魔に感じませんか。

同じことをCounterで書くと、for文ごと消えてなくなります。

from collections import Counter

words = ["python", "flask", "python", "django", "flask", "python"]
counts = Counter(words)
print(counts)
Counter({'python': 3, 'flask': 2, 'django': 1})

リストを渡しただけで、中身を数えて辞書のような形で返してくれました。表示がCounterになっているだけで、使い方は辞書とほとんど変わりません。

most_commonで多い順に取り出す

Counterが素の辞書より優れているのは、並べ替えがすでに用意されている点です。most_commonを呼ぶと、多い順に並んだ組を受け取れます。

続けて2行だけ実行してみます。

print(counts.most_common(2))
print(counts["ruby"])
[('python', 3), ('flask', 2)]
0

上位2件を取り出せました。引数を省くと全件が多い順で返ってきます。

もうひとつ注目してほしいのが2行目です。存在しないキーを聞いてもエラーにならず、0が返っています。

数えるという役目に合わせて、まだ1回も出ていないものは0とみなす設計になっているわけです。この気配りは、使ってみると地味に効きます。

【関連記事】Pythonのリスト内包表記を使いこなせ!3行のループを1行にまとめる書き方

KeyErrorから解放されるdefaultdict

次はdefaultdictです。辞書の値としてリストや数値をためていく場面で活躍します。

チームごとにメンバーをまとめる処理を考えてみましょう。素直に書くと、こうなります。

groups = {}
groups["A"].append("田中")

残念ながら、これは動きません。実行すると次のエラーが出ます。

KeyError: 'A'

キーAがまだ存在しないので、appendを呼ぶ相手がいないのです。初心者が高い確率で踏むエラーのひとつになります。

【関連記事】PythonでKeyErrorが出る理由とは?辞書dictで初心者がハマるポイント

defaultdictを使うと、この存在確認そのものが不要になります。

from collections import defaultdict

groups = defaultdict(list)
members = [("A", "田中"), ("B", "鈴木"), ("A", "佐藤")]

for team, name in members:
    groups[team].append(name)

print(dict(groups))
{'A': ['田中', '佐藤'], 'B': ['鈴木']}

存在しないキーに触れた瞬間、空のリストが自動で用意されました。だからappendがそのまま通るわけです。

ここで渡しているlistは、最初の値を作るための関数です。defaultdict(list)なら空のリスト、defaultdict(int)なら0が入ります。

ひとつだけ注意点があります。参照しただけでもキーが作られるので、確認のつもりで書いた1行が、静かにキーを増やしてしまうことがあるのです。

両端の出し入れが速いdeque

3つめはdequeです。読み方はデックで、両端キューという意味になります。

リストの先頭に何かを足したり、先頭から取り出したりする処理を書いたことはありますか。実はそれ、リストがいちばん苦手とする操作です。

Pythonのリストは内部的に配列で、要素が順番にきっちり並んでいます。先頭を1つ抜くと、後ろの全要素を1つずつ前へずらさなければなりません。

どれくらい違うのか、10万件で測ってみました。

import timeit

n = 100000
t_list = timeit.timeit("d.pop(0)", setup=f"d=list(range({n}))", number=n)
t_deque = timeit.timeit(
    "d.popleft()",
    setup=f"from collections import deque; d=deque(range({n}))",
    number=n,
)
print(f"list.pop(0)   {t_list:.3f} 秒")
print(f"deque.popleft {t_deque:.4f} 秒")

手元のPython 3.11での結果がこちらです。

list.pop(0)   0.755 秒
deque.popleft 0.0027 秒

同じ処理で280倍ほどの差がつきました。しかも件数が増えるほど、この差はさらに開いていきます。

Python公式Wikiの計算量ページでも、先頭付近への挿入や削除はリストのコストが大きい操作として説明されています。両端で出し入れするならdequeを検討してほしい、という案内も添えられています。

【関連記事】「実行時間が終わらない…」を卒業する!あなたのコードを100倍速くする計算量の考え方

maxlenで直近だけを残す

dequeにはもうひとつ便利な機能があります。maxlenを指定すると、決めた件数を超えたぶんが古い側から自動で消えていきます。

短いコードで確かめてみましょう。

from collections import deque

history = deque(maxlen=3)
for line in ["1行目", "2行目", "3行目", "4行目", "5行目"]:
    history.append(line)

print(history)
deque(['3行目', '4行目', '5行目'], maxlen=3)

5件入れたのに、残ったのは最後の3件だけでした。直近のログだけを持っておきたいときに、古い要素を消す処理を自分で書かずに済みます。

名前で読めるタプルnamedtuple

4つめはnamedtupleです。タプルの各要素に名前を付けられる仕組みになります。

座標を扱うコードで考えてみましょう。普通のタプルだと、p[0]が何を指しているのかはコードを眺めただけでは分かりません。

名前を付けると、その悩みが消えます。

from collections import namedtuple

Point = namedtuple("Point", ["x", "y"])
p = Point(10, 20)

print(p)
print(p.x, p.y)
print(p[0])
print(p._asdict())
Point(x=10, y=20)
10 20
10
{'x': 10, 'y': 20}

p.xと書けるようになりました。それでいてp[0]も使えるので、これまでタプルとして扱っていたコードもそのまま動きます。

_asdictを呼べば辞書に変換できるため、JSONにして返すときも困りません。中身は書き換えられないので、うっかり上書きする事故も防げます。

【関連記事】Pythonのリスト(list)とタプル(tuple)、どっちを使う? それぞれの違いを徹底解説

似た役割のものにdataclassがあります。あとから値を書き換えたいならdataclass、書き換えたくないならnamedtupleという分け方が目安になります。

【関連記事】Pythonのdataclassとは?クラスの定義がぐっと短くなる書き方を初心者向けに解説

4つの使い分けを整理する

ここまで出てきた4つを、選ぶときの目印とあわせて並べておきます。

名前 置き換えるもの 使いどころ
Counter 数えるための辞書 登場回数の集計、上位の抽出
defaultdict 初期値つきの辞書 キーごとにリストや数値をためる
deque 先頭を触るリスト 両端の出し入れ、直近N件の保持
namedtuple 意味のあるタプル 読みやすい戻り値、固定の値の組

迷ったときは、真ん中の列を見てください。いま書いているコードがそこに当てはまるなら、出番だと考えて大丈夫です。

残りの5つも名前だけ覚えておく

先ほど表示された9つのうち、まだ触れていない5つにもひとことずつ添えておきます。

名前 どんなもの
OrderedDict 順序を保つ辞書。今のdictが順序を保つため出番は減った
ChainMap 複数の辞書をまとめて1つに見せる。設定の上書きなどに使う
UserDict / UserList / UserString 辞書やリストを継承して自作するときの土台

いま覚える必要はありません。名前だけ頭の隅に置いておけば、必要になったときに調べられます。

実務で踏んだ落とし穴

私は10年ほどエンジニアとして開発に関わってきましたが、collectionsで一度だけ痛い目にあったことがあります。

defaultdictを設定値の入れ物として使っていたときの話です。存在チェックのつもりで書いた参照が、裏で次々と空の値を作っていました。

ログ集計のバッチで、メモリ使用量がじわじわ増えていく。原因にたどり着くまでに、まる1日かかりました。

犯人は、if config[key]という何気ない1行です。defaultdictでは、この参照だけでキーが増えていきます。

存在を確かめたいだけなら、inを使うかgetを使ってください。それ以来、defaultdictを他の関数へ渡すときは、最後にdictへ戻す習慣がつきました。

便利な道具ほど、素のdictとの違いを1つだけ覚えておくと安全です。

どこから使い始めればいいか

全部を一度に覚える必要はまったくありません。おすすめはCounterからです。

数を数える処理は、学習中でもすぐに出てきます。一度使えば、次から自然と手が伸びるようになります。

次に進むならdefaultdictです。KeyErrorで困った経験があるなら、その場面がそのまま出番になります。

dequeとnamedtupleは、必要になったときで十分でしょう。こんな道具があったな、と思い出せればそれで役目は果たせます。

collectionsを覚えると、書くコードが減ります。そして減ったぶんだけ、バグが入り込む余地も減っていくのです。

まずは今日書いたコードの中に、数える処理や辞書に値をためる処理がないか探してみてください。見つかったら、そこが最初の練習台です。

ここまでお読みいただきありがとうございました。

参考情報

次のアクション

記事で学んだ内容を実際に動かしてみよう

Python WebAcademyでは、ブラウザ上でコードを書きながら基礎から実践まで体系的に学べます。

Python WebAcademyの学習画面

あわせて読む

関連記事

ブログ一覧へ

Python学習ロードマップ

まずはこの3講座から

記事で気になったテーマを、順番に手を動かしながら学べます。

ロードマップを見る