概要(辞書の値にリストを持たせて「グループ化・順次追加」を安全に扱う)
辞書の中にリストを入れると、「キーごとに複数の要素」を自然に管理できます。たとえばカテゴリごとの商品一覧、ユーザーごとの履歴、タグ別の投稿IDなど。重要なのは「未初期化キーへの安全な追加(setdefault か defaultdict)」「ミュータブル共有の落とし穴(コピーの階層)」「走査と更新の分離(副作用の管理)」の3点です。これを押さえると、JSONやログ処理でも壊れずに伸びるコードが書けます。
基本の操作(ここが重要)
追加・更新(キーごとのリストへ要素を足す)
辞書の値がリストなら append や extend で足します。未登録キーは setdefault を使えば一行で安全に初期化できます。
items_by_cat = {}
def add_item(cat, item):
items_by_cat.setdefault(cat, []).append(item)
add_item("coffee", "latte")
add_item("coffee", "espresso")
add_item("tea", "earl grey")
print(items_by_cat) # {'coffee': ['latte','espresso'], 'tea': ['earl grey']}
Python既存かどうかを if で判定するより、setdefault を使う方が短く、競合にも強い書き方です。
まとめて追加(extend で一括)
複数要素を同時に足すなら extend が簡潔です。append との違いは「リストを“中身”として足す」点。
logs = {}
logs.setdefault("INFO", []).extend(["start", "ready"])
logs.setdefault("ERROR", []).extend(["failed"])
Python欠損を避ける走査(get を使って空リスト扱い)
存在しないキーで例外を出さないよう、走査時は .get(key, []) を使うと安全です。
for msg in logs.get("WARN", []):
print(msg) # WARN が無くても空扱いで安全
Python重要ポイントの深掘り(初期化・コピー・副作用)
setdefault と defaultdict の使い分け
- setdefault は「その場だけ」初期化+取得。既存辞書に対する局所的な追加に向く。
- collections.defaultdict(list) は「未登録キーは自動で []」になる辞書。広範に使うならこちらが楽。
from collections import defaultdict
items_by_user = defaultdict(list)
items_by_user["alice"].append(101) # 自動で [] が割り当てられる
Python既存の dict をそのまま使うなら setdefault、全体の生成時から整えるなら defaultdict。
ミュータブル共有の罠(コピーの階層を意識する)
辞書の値がリストだと「参照共有」による伝播が起こります。外側だけのコピー(shallow)では内部リストは共有されたまま。編集対象の階層だけ防御的コピーを取りましょう。
data = {"coffee": ["latte", "espresso"], "tea": ["earl grey"]}
shallow = data.copy() # 外側だけ別、内側は共有
shallow["coffee"].append("mocha")
print(data["coffee"]) # 影響する(共有されている)
# 防御的:値(リスト)までコピー
safe = {k: v[:] for k, v in data.items()}
safe["coffee"].append("cappuccino")
print(data["coffee"]) # 影響しない
Python完全独立が必要なら deepcopy。ただし重いので、必要な階層だけ手動コピーが現実的です。
走査しながら破壊的更新しない(副作用を分離)
ループ中に同じ辞書のリストを直接書き換えると、反復順序や条件が崩れます。まず「読む→決める」、次に「書く」を分離すると安全です。
to_add = []
for item in data.get("coffee", []):
if item.startswith("e"): # 例:条件に合うものを別に集める
to_add.append(item.upper())
data.setdefault("COFFEE_UPPER", []).extend(to_add) # ループ外で更新
Python走査・整形・集計の定番パターン
全キーを回る・キー順に並べる
表示や出力で「キー順」を安定させたいなら、sorted で回します。
for cat in sorted(items_by_cat):
print(cat, items_by_cat[cat])
Pythonリストの重複排除・ソート
辞書値のリストは重複が溜まりやすいので、set と sorted で正規化します。
for k, v in items_by_cat.items():
items_by_cat[k] = sorted(set(v))
Pythonフラット化(全要素をひとつのリストへ)
キー情報を残すかどうかで書き方が変わります。
# 値だけフラット化
all_items = [x for v in items_by_cat.values() for x in v]
# キーも保持してタプル化
pairs = [(k, x) for k, v in items_by_cat.items() for x in v]
Python条件付き抽出・集計
キー・値に条件をかける基本形を身につけます。
# 例:len が 2 以上のカテゴリだけ、件数を出す
counts = {k: len(v) for k, v in items_by_cat.items() if len(v) >= 2}
Python挿入・削除・マージ(整合性を保つ作法)
キーの削除と空リスト整理
値の削除後に「空リストを持つキー」を消して整えると、後続処理が楽になります。
def remove_item(d: dict, key, item):
vals = d.get(key, [])
if item in vals:
vals.remove(item)
if not vals:
d.pop(key, None)
remove_item(items_by_cat, "coffee", "latte")
Python辞書同士のマージ(値リストを結合)
右を優先して結合し、重複を排除します。
def merge_dicts(a: dict, b: dict) -> dict:
out = {k: v[:] for k, v in a.items()} # 値までコピー
for k, v in b.items():
out.setdefault(k, []).extend(v)
# 重複排除+整形
for k in out:
out[k] = sorted(set(out[k]))
return out
Pythonインデックス付き更新(位置で編集)
位置指定の更新は範囲チェックを忘れずに。
def set_at(d: dict, key, idx, value):
vals = d.get(key)
if vals is None:
raise KeyError(f"{key} not found")
if not (0 <= idx < len(vals)):
raise IndexError(f"index {idx} out of range")
vals[idx] = value
PythonJSON・入れ子構造での実務ポイント
JSON 読み込み後の「存在しないキー」の扱い
API レスポンスの欠損に備え、.get と setdefault を併用すると頑健になります。
resp = {"users": [{"name": "alice"}, {"name": "bob"}]} # 例
names = [u.get("name", "") for u in resp.get("users", [])]
Python深い入れ子での初期化(段階的 setdefault)
段階的に setdefault を使えば、ネストの初期化が短く書けます。
store = {}
store.setdefault("tokyo", {}).setdefault("coffee", []).append("latte")
store.setdefault("tokyo", {}).setdefault("coffee", []).append("espresso")
Python部分コピーで安全加工
テンプレートを汚さず加工するなら、編集対象の値リストだけコピーしてから操作します。
template = {"tags": ["a", "b"], "meta": {"author": "hanako"}}
cfg = dict(template) # 外側はシャロー
cfg["tags"] = list(template["tags"]) # 値のリストを独立
cfg["tags"].append("c")
Python例題で身につける(定番から一歩先まで)
例題1:カテゴリ別に商品を追加・重複排除して出力
items = {}
for cat, name in [("coffee","latte"), ("tea","earl"), ("coffee","latte"), ("coffee","mocha")]:
items.setdefault(cat, []).append(name)
for k in items:
items[k] = sorted(set(items[k]))
print(items) # {'coffee': ['latte','mocha'], 'tea': ['earl']}
Python例題2:辞書のリストへ集計して格納(ユーザー別件数)
logs = [{"user":"alice","id":1}, {"user":"bob","id":2}, {"user":"alice","id":3}]
by_user = {}
for row in logs:
by_user.setdefault(row["user"], []).append(row["id"])
counts = {u: len(ids) for u, ids in by_user.items()}
print(counts) # {'alice': 2, 'bob': 1}
Python例題3:入れ子辞書で段階的初期化+更新
store = {}
def add(city, cat, item):
store.setdefault(city, {}).setdefault(cat, []).append(item)
add("Tokyo", "coffee", "latte")
add("Tokyo", "coffee", "espresso")
add("Osaka", "tea", "hojicha")
print(store)
Python例題4:安全なマージ(値リスト結合+正規化)
a = {"coffee": ["latte", "espresso"], "tea": ["earl"]}
b = {"coffee": ["mocha", "latte"], "juice": ["orange"]}
def merge(a, b):
out = {k: v[:] for k, v in a.items()}
for k, v in b.items():
out.setdefault(k, []).extend(v)
for k in out:
out[k] = sorted(set(out[k]))
return out
print(merge(a, b))
Pythonまとめ
辞書内のリスト操作は「キーごとに複数要素を持つ」構造を簡潔に扱う最良の手段です。未登録キーへの追加は setdefault(広範なら defaultdict)、ミュータブル共有の落とし穴は「値までコピー」で回避、走査と更新は分離して副作用を抑える。重複排除・ソート・フラット化・条件抽出・マージの基本形を身につければ、JSON処理やログ集計、カテゴリ管理まで、短く明快で壊れないコードになります。
