Python | データ構造強化:辞書内のリスト操作

Python Python
スポンサーリンク

概要(辞書の値にリストを持たせて「グループ化・順次追加」を安全に扱う)

辞書の中にリストを入れると、「キーごとに複数の要素」を自然に管理できます。たとえばカテゴリごとの商品一覧、ユーザーごとの履歴、タグ別の投稿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
Python

JSON・入れ子構造での実務ポイント

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処理やログ集計、カテゴリ管理まで、短く明快で壊れないコードになります。

タイトルとURLをコピーしました