Python|Counterでゼロ件保証を正しく設計する考え方
この記事のポイント
- Counter利用時のゼロ件保証責務をレビューできる
- 集計初期化責務と出力整形責務を分離設計できる
- 集計漏れ・暗黙依存・実装事故を防ぐレビュー観点が身につく
そもそもCounterとは
Python標準ライブラリcollections.Counter
は、要素の出現回数を自動的にカウントする辞書型です。
from collections import Counter
data = ["apple", "orange", "apple"]
result = Counter(data)
# Counter({'apple': 2, 'orange': 1})
特徴は以下です。
- 要素出現回数の自動集計
- 存在しないキーは自動的に0返却
- 累積処理・頻度計測に最適
初期ゼロ保証の自動性は便利である反面、設計次第で責務崩壊リスクも抱えます。
なぜこれをレビューするのか
現場で頻発する失敗は以下です。
- ゼロ保証の過信による集計漏れ
- 集計責務の曖昧化
- 出力責務がCounterに依存しすぎる
- 欠損許容設計ミスの温床化
レビューアーは「ゼロ件保証は集計責務なのか出力整形責務なのか?」を読み取る必要があります。
レビューアー視点
- 集計責務範囲が整理されているか
- 出力形式のゼロ保証が明示されているか
- Counterの自動ゼロ保証を過信していないか
- 欠損補完処理責務が分離設計されているか
- 出力側コードの事故リスクが最小化されているか
開発者視点
- Counterは純粋な集計器として使う
- ゼロ補完は出力整形責務に分離
- 欠損初期化ロジックを集計中に混入させない
- 出力仕様を意識した整形専用処理を用意
- 欠損保証範囲をレビュー時に明文化
良い実装例
なぜこの実装が良いのか
- Counterは純粋集計責務のみ担当
- ゼロ保証は出力整形責務で担当
- 出力要件に応じた責務分離
- 欠損補完仕様がコードに明示
# counter_with_zero_filling.py
from collections import Counter
def count_words(words: list[str], known_words: list[str]) -> dict[str, int]:
counter = Counter(words)
return {word: counter.get(word, 0) for word in known_words}
補足
「Counterはカウント専用、整形責務は別」が基本設計です。レビューアーは整形責務分離有無を重点確認します。
レビュー観点
- Counterは純粋に集計専用で使われているか
- 欠損補完責務が整形層に分離されているか
- 集計時の初期化責務が曖昧化していないか
- 出力要件が型・仕様上で保証可能か
- 欠損保証仕様がレビューで即座に確認できるか
良くない実装例: ケース1(Counter自体に出力仕様依存)
# bad_counter_as_viewmodel.py
from collections import Counter
def count_status(statuses: list[str]) -> Counter:
counter = Counter(statuses)
return counter
@ReviewerCounterオブジェクトをそのまま出力責務に流用しています。整形責務を分離してください。
問題点
- 出力層でCounter内部仕様依存
- 欠損保証仕様が曖昧
- 呼び出し側が型仕様事故リスクを背負う
改善例
# good_counter_as_pure_aggregator.py
def count_status(statuses: list[str], known_statuses: list[str]) -> dict[str, int]:
counter = Counter(statuses)
return {status: counter.get(status, 0) for status in known_statuses}
出力仕様はCounter以外の安定型(dict等)で明示化するのが設計原則です。レビューでは型安全性を確認します。
良くない実装例: ケース2(初期値補完を集計中に混入)
# bad_inline_initialization.py
from collections import Counter
def count_categories(categories: list[str], known: list[str]) -> dict[str, int]:
counter = Counter({key: 0 for key in known})
counter.update(categories)
return dict(counter)
@Reviewer集計前に初期化責務が混入し集計責務が肥大化しています。整形処理に分離してください。
問題点
- 集計開始前に欠損補完ロジック混在
- 集計責務が整形責務を内包
- 再利用性・保守性崩壊
改善例
# good_separated_initialization.py
def count_categories(categories: list[str], known: list[str]) -> dict[str, int]:
counter = Counter(categories)
return {key: counter.get(key, 0) for key in known}
集計処理は純粋に「データから出現回数を取る」だけに限定が原則です。レビューではこの責務線引きを明確に読みます。
良くない実装例: ケース3(欠損補完を呼び出し側に放置)
# bad_caller_fill_missing.py
def count_items(items: list[str]) -> dict[str, int]:
return dict(Counter(items))
# 呼び出し側で補完
counts = count_items(["A", "B", "B"])
for key in ["A", "B", "C"]:
count = counts.get(key, 0)
@Reviewer欠損補完責務が呼び出し側へ漏洩しています。API提供側で整形責務を吸収してください。
問題点
- 呼び出し契約に欠損補完要求が漏洩
- API利用者の認知負荷増大
- 再利用障害
改善例
# good_api_level_filling.py
def count_items(items: list[str], known_keys: list[str]) -> dict[str, int]:
counter = Counter(items)
return {key: counter.get(key, 0) for key in known_keys}
API利用者は欠損補完を意識不要が理想設計です。レビューではAPI契約の責務線引きを確認します。
良くない実装例: ケース4(defaultdict代替で設計事故)
# bad_counter_vs_defaultdict.py
from collections import defaultdict
def count_words(words: list[str]) -> dict[str, int]:
counter = defaultdict(int)
for word in words:
counter[word] += 1
return dict(counter)
@ReviewerCounterが提供する集計専用APIを使わず汎用defaultdictを利用しています。集計責務にはCounterを優先してください。
問題点
- 集計器としての責務明示性消失
- 汎用defaultdictで意図が読めなくなる
- 再利用設計困難化
改善例
# good_use_counter.py
from collections import Counter
def count_words(words: list[str]) -> dict[str, int]:
return dict(Counter(words))
集計=Counter、状態保持=dict、初期化責務=整形層という責務線引きがレビュー基本方針です。
観点チェックリスト
まとめ
Counter設計レビューは「集計責務 vs 出力保証責務」の線引き訓練です。
レビューアーは常に
- Counterが集計専用になっているか?
- 欠損保証は出力整形に分離されているか?
- 呼び出し契約が自然に閉じているか?
を読み取り、「設計事故の温床となる責務混在を早期遮断する」レビュー習慣を徹底していきます。
Counterレビューは集計系設計教育の極めて有効な教材領域です。