順番に読み進めながら学べます

collections — Counter / defaultdict / deque / namedtuple

Counterの出現回数集計、defaultdictの欠損キー対策、dequeの両端O(1)操作とリングバッファ、namedtupleの名前付きレコードを実例で学べます。

collectionsモジュールには、list / dict / tuple の使いにくいところを補う特殊なデータ構造が集まっています。本記事では実プロジェクトでよく使うCounter / defaultdict / deque / namedtupleの 4 つを順に整理します。

4 つのデータ構造の役割分担

それぞれが「素の組み込み型では書きにくい場面」を簡潔にするために使用します。下の表で全体像を掴んでから、各セクションで具体的な使い方を見ていきます。

collections の 4 つの型
Counter出現回数を集計defaultdict欠損キーで自動初期化deque両端の追加・取り出し O(1)namedtupleタプルに名前を付ける
Counterは集計、defaultdictは欠損キー対策、dequeは両端の高速操作、namedtupleは名前付きレコード。すべて素のlist / dict / tupleの上に使いやすい層を被せた拡張型。
解決する問題代わりに使えるもの
Counterリストの要素を 1 つずつ数えるループが冗長for 文 + dict での集計
defaultdict新しいキーを使う前に if x not in d: d[x] = ... が必要dict.setdefault(...)
dequelist.insert(0, ...) や list.pop(0) が遅い (O(n))list(小さいデータなら問題なし)
namedtupleタプル要素の意味が位置でしか分からないdataclass や dict(重め)

Counter — 出現回数を 1 行で集計する

Counterイテラブル(リスト・文字列など)を渡すと、各要素の出現回数を辞書として返してくれるクラスです。普通の dict を使うと「キーが既にあるか確認して、無ければ 1 で初期化、あれば +1」というループを書く必要がありますが、Counter ならCounter(リスト)の 1 行で同じことができます。

戻り値はdict のサブクラスなので、counter["apple"]のように普通の dict と同じ感覚で値を取り出せます。さらに.most_common(N)「上位 N 件のタプルのリスト」を返してくれて、ランキング表示に便利です。

Counter の働き
リスト[apple, banana, apple, ...]Counter(リスト)Counter{apple: 3, banana: 2, ...}集計
イテラブルを渡すだけで、各要素の出現回数を持った dict サブクラスが返る。most_common(N)で頻度順の上位 N 件を取り出せるので、ランキング表示が 1 行で書ける。

6 件の購入履歴から商品の出現回数を Counter で集計します。普通の for ループで書く代わりに 1 行で済むことを体感します。

① collections から Counter を読み込んでください

② 購入履歴のリスト["apple", "banana", "apple", "cherry", "banana", "apple"]を用意してください

③ Counter にリストを渡して集計結果を作り、カウント: ◯の形でそのまま表示してください

appleの出現数をapple の数: ◯の形で表示してください(dict と同じ角括弧アクセスで取れます)

最頻 2 件most_commonで取り出して最頻 2 件: ◯の形で表示してください

(正しく実行できれば解説が表示されます)

Python エディタ

コードを実行してください

defaultdict — 欠損キーを自動で初期化する

defaultdict「キーが存在しないときに自動で初期値を入れてくれる」辞書です。普通の dict だと、新しいキーに対してif key not in d: d[key] = []のような存在チェック → 初期化を毎回書く必要がありますが、defaultdict は作成時に「初期値を作る関数」を渡しておくだけで、欠損キーへのアクセス時に自動でその関数を呼んで初期値を作ってくれます。

よく使うのはdefaultdict(list)(欠損キーには空リスト)とdefaultdict(int)(欠損キーには 0)の 2 パターンです。引数は「呼び出すと初期値を返す関数」で、listintも引数なしで呼ぶと空リスト / 0 を返すので、そのまま渡せます。

dict と defaultdict の違い
普通の dictd["x"] (キーが無い)→ KeyErrorif x not in d: d[x] = [] が必要defaultdict(list)d["x"] (キーが無い)→ 自動で空リスト []そのまま .append(...)が書ける
普通の dictは欠損キーにKeyErrordefaultdict作成時に渡した関数を呼んで初期値を自動で入れてくれる。listを渡せば空リスト、intを渡せば0setを渡せば空集合が初期値になる。

Counter との使い分け

「単純に数を数えるだけ」ならCounterのほうが簡潔(most_commonも付いてくる)。「グループ化してリストや集合を作る」など、初期値が空コンテナの場合はdefaultdict(list) / defaultdict(set)が向きます。

色ごとに果物をグループ化します。defaultdict(list)を使うと、各色のキーが存在しない最初のアクセスで自動的に空リストが用意されるので、存在チェック無しで .appendできます。

① collections から defaultdict を読み込んでください

値の初期値が空リストになる defaultdict を作ってください

③ 次の 3 件を順に登録してください

- "red""apple"

- "yellow""banana"

- "red""cherry"

④ 結果を普通の dict に変換して 色ごと: ◯の形で表示してください(defaultdict のまま print すると repr が見にくいため)

Python エディタ

コードを実行してください

deque — 両端の追加・取り出しが O(1)

dequedouble-ended queue、デック)は両端から要素を追加・取り出しできる、リスト風のデータ構造です。普通のlistappendpopで右端の操作はできますが、list.insert(0, x) や list.pop(0) のように左端を操作すると、内部で全要素を 1 つずつシフトするためO(n)(要素数が増えれば遅く)なります。

dequeはこの両端の操作が O(1)(要素数によらず一定時間)で済む設計で、キュー / 履歴バッファ / 直近 N 件の保持など左右両方から出し入れする処理で重宝します。特に便利なのがmaxlen 引数で、最大長を指定すると、それを超える追加で自動的に反対側の要素が落ちるリングバッファとして動きます。

list と deque の操作コスト比較
list左端追加 / 取り出し全要素を 1 つシフト→ O(n) 遅いdeque両端 append / popポインタ操作のみ→ O(1) 速い
list は左端の操作が O(n)(全要素シフト)、deque は両端 O(1)。直近 N 件を保持するときもdeque(maxlen=N)古いものから自動的に落ちるリングバッファとして使える。
メソッド効果コスト
append(x)右端に追加O(1)
appendleft(x)左端に追加O(1)
pop()右端から取り出しO(1)
popleft()左端から取り出しO(1)
maxlen=N最大長を指定(リングバッファ風)超えると反対端が自動で落ちる
deque の主要メソッド
appendleft(x)deque[a, b, c]append(x)popleft()pop()
中央の dequeに対し、左端はappendleft / popleft右端はappend / popで出し入れする。矢印の向きが追加(deque へ)取り出し(deque から外へ)の方向を示す。

直近 3 件のアクセスログを deque(maxlen=3) で保持します。5 回 append しても最後の 3 件しか残らないことを確認します。

① collections から deque を読み込んでください

最大長 3の deque を空で作ってください

0から4までの整数を順に appendしてください(5 件追加)

④ 結果を最後の 3 件: ◯の形で表示してください(古い 0, 1 は溢れて消えるはずです)

Python エディタ

コードを実行してください

タスクキューの先頭と末尾から要素を取り出しますpopleftpopで、左右どちらから取り出すかを切り替えられることを確認します。

① collections から deque を読み込んでください

② タスク["タスクA", "タスクB", "タスクC", "タスクD"]の 4 件を deque にしてください

popleft()先頭のタスクを取り出し、先頭タスク: ◯の形で表示してください

pop()末尾のタスクを取り出し、末尾タスク: ◯の形で表示してください

⑤ 残った deque を残り: ◯の形で表示してください

Python エディタ

コードを実行してください

namedtuple — タプルに名前を付ける軽量レコード

namedtuple「フィールドに名前を付けたタプル」を 1 行で定義できる関数です。普通のタプル(3, 4)位置でしかアクセスできないp[0] / p[1])ので、p[0]が x なのか y なのかをコードを読む側が覚えておく必要があります。namedtuple ならp.x / p.y で意味のある名前で取り出せるので、可読性が大きく上がります。

さらにタプルとしての互換性も保たれているので、p[0]の位置アクセスや*pでのアンパックも普通のタプルと同じように動きます — 既存のコードを壊さずに「名前」だけを足せる、軽量な拡張です。

namedtuple の特徴
Point = namedtuple('Point', ['x', 'y'])p = Point(3, 4)名前でp.x / p.y位置でp[0] / p[1]辞書化p._asdict()
1 行でクラス相当のものを定義でき、位置アクセス(p[0])と名前アクセス(p.x)の両方が使える。_asdict()で辞書にも変換できるので、JSON 化や pprint との相性も良い。

dataclass との使い分け

変更不可の軽量レコードが欲しいだけならnamedtupleメソッドやデフォルト値・型ヒントを細かく書きたいならdataclass(次々回扱います)。namedtuple はタプルとしても振る舞う互換性が強みで、def f() -> tuple[int, int]:のような既存の関数戻り値の置き換えに使いやすいです。

Point クラス相当の namedtuple を定義して、名前で座標を取り出します

① collections からnamedtupleを読み込み、クラス名 "Point"、フィールド ["x", "y"]の namedtuple を作ってください

x=3, y=4のインスタンスpを作ってください

x: 3 y: 4の形でp.xp.y名前アクセスで表示してください

Python エディタ

コードを実行してください

namedtuple の追加機能 — repr / インデックス / _asdict

namedtuple はクラスとしての名前アクセスに加えて、タプル互換のインデックスアクセス読みやすい repr辞書化メソッドという 3 つの追加機能を持っています。これらはデバッグ・既存 API との互換・JSON 化などで用います。

実践 1 で作った pに対して、3 種類の追加アクセスを試します。

print("repr:", p)repr: Point(x=3, y=4)の形のままインスタンスを表示してください

p[0]p[1]インデックスアクセス位置: 3 4の形で表示してください

p._asdict()辞書: {'x': 3, 'y': 4}の形で辞書化したものを表示してください

Python エディタ

コードを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1リストnums要素ごとの出現回数1 行で取得したい場合、最も簡潔なのはどれですか?

Q2defaultdict(list)を作った後、初めて使うキー d["x"]の値はどうなりますか?

Q3直近 5 件だけ保持したいときに最も向いているのはどれですか?