導入:dict を「使う」人と、マッピングを「設計する」人の差
Python を書く人なら、誰もが dict を毎日使います。しかし、現場で評価が分かれるのは「dict を使えるか」ではなく、**「dict が体現している『マッピング』という抽象を理解し、必要に応じて自分のマッピング型を設計できるか」**です。
なぜ集計コードに defaultdict や Counter を使うと一気に読みやすくなるのか。なぜ設定の優先順位は ChainMap で「コピーせずに」重ねられるのか。なぜ公開 API で内部の dict をそのまま返すと事故になり、MappingProxyType が必要なのか。そして——なぜ dict を継承してメソッドを上書きすると、半分しか効かないのか。これらはすべて、「マッピングというプロトコル(抽象)」を理解していれば設計判断として説明できることです。
本記事は、世界中で読まれている Real Python の "Python Mappings" が扱う範囲——マッピングの定義、collections.abc の抽象基底クラス、標準ライブラリのマッピング群、自作マッピング——を土台にしつつ、そこから先の「本番で効く設計知」まで踏み込みます。具体的には、
- マッピング・プロトコル(
Mapping/MutableMapping)の正確な契約と、継承で「タダで手に入るメソッド」 defaultdict/Counter/OrderedDict/ChainMap/MappingProxyTypeの使い分けと落とし穴dictを直接継承してはいけない理由と、UserDict/MutableMappingによる正しい自作- 自作オブジェクトをキーにするための
__hash__/__eq__契約 - 構造的パターンマッチ、本番の落とし穴、そしてシステム境界での型検証
筆者は、経済産業大臣賞を受賞した B2B SaaS のバックエンドを Python / Flask / SQLAlchemy / PostgreSQL で設計・実装し、Router → UseCase → Repository の厳格な層分離で本番運用してきました。「外部から来た dict を信頼しない」「内部状態を読み取り専用で公開する」といった本記事の原則は、すべてその実戦から得たものです。dict の基礎(ハッシュ可能性・挿入順保持・計算量)は前作の Python のデータ型 完全ガイド で扱ったので、本記事は**その一段上、「マッピングを設計する」**ところに集中します。
💡 対象バージョン:Python 3.12 / 3.13 を前提とします(記述の大半は 3.10 以降で有効)。バージョン依存の機能には導入バージョンを明記します。
1. マッピングとは何か:dict は「実装の一つ」にすぎない
マッピング(mapping)とは、「キーから値への対応」を表すコレクションです。Python の世界では dict が最も有名で高速な実装ですが、dict だけがマッピングではありません。defaultdict、Counter、ChainMap、OrderedDict、MappingProxyType、そしてあなたが自作するクラスも、すべて「マッピング」です。
ここで重要なのは、「マッピングである」とは「特定の振る舞い(プロトコル)を満たす」ことであって、「dict を継承していること」ではない、という点です。これは「具体的な実装ではなく抽象に依存する」という設計原則(ETC: Easy To Change)の、Python における具体例です。
この「振る舞いの契約」を形にしたのが、collections.abc の抽象基底クラス(ABC)です。
from collections.abc import Mapping, MutableMapping
isinstance({}, Mapping) # → True
isinstance({}, MutableMapping) # → True
from types import MappingProxyType
isinstance(MappingProxyType({}), Mapping) # → True
isinstance(MappingProxyType({}), MutableMapping) # → False ← 読み取り専用だから
最後の例が、抽象の威力です。MappingProxyType(読み取り専用)は Mapping だが MutableMapping ではない。だから関数の引数で Mapping を要求すれば「読むだけ」を、MutableMapping を要求すれば「書き換える」ことを、型で表明できます。
def render(config: Mapping[str, str]) -> str:
# config を変更しないことを「型」で約束している(読み取り専用契約)
return "\n".join(f"{k}={v}" for k, v in config.items())
2. マッピング・プロトコルの「契約」:何を実装すれば何がタダで手に入るか
ここが Real Python の記事の核心であり、自作マッピングを理解する鍵です。collections.abc の ABC を継承すると、少数の「抽象メソッド」を実装するだけで、大量の「ミックスインメソッド」が自動的に手に入ります。
| 継承する ABC | 自分で実装する抽象メソッド | タダで手に入るミックスイン |
|---|---|---|
| Mapping(読み取り専用) | __getitem__ / __iter__ / __len__ | __contains__ / keys / items / values / get / __eq__ / __ne__ |
| MutableMapping(読み書き) | 上記 + __setitem__ / __delitem__ | 上記 + pop / popitem / clear / update / setdefault |
つまり、MutableMapping を継承すれば、わずか 5 メソッド(__getitem__ / __setitem__ / __delitem__ / __iter__ / __len__)を実装するだけで、dict とほぼ同じ API を持つ完全なマッピングが完成します。しかも、get も update も __contains__ も、あなたが実装した 5 メソッドを経由して動くため、振る舞いが一貫します。これがプロトコル設計の美しさです。
💡 なぜこれが「世界最高峰」の設計につながるのか:ミックスインは DRY(Don't Repeat Yourself)の極致です。
getやupdateのロジックを自分で書けば、必ずどこかでバグります。ABC を継承すれば、それらは一度だけ正しく書かれた標準実装を使い回せる。あなたは「このマッピング固有の本質(5 メソッド)」だけに集中できます。これは SRP(単一責任)の実践でもあります。
3. 標準ライブラリの強力なマッピング群(ここで生産性が変わる)
dict で何でも書けますが、目的に合ったマッピングを選ぶと、コードが宣言的になり、意図が一目で伝わります。これは KISS(単純さ)の実践です。
3-1. defaultdict:キー欠損を「初期値の自動生成」で吸収する
集計・グルーピングの定番。存在しないキーにアクセスすると、default_factory が初期値を作って挿入します。
from collections import defaultdict
groups = defaultdict(list)
for name in ["apple", "avocado", "banana"]:
groups[name[0]].append(name) # キーがなければ [] を自動生成して append
# → defaultdict(<class 'list'>, {'a': ['apple', 'avocado'], 'b': ['banana']})
dict なら groups.setdefault(name[0], []).append(name) と書くところを、宣言的に置き換えられます。ただし落とし穴があります。
counts = defaultdict(int)
_ = counts["nonexistent"] # ← 読んだだけのつもりが、キーが作られる!
print(dict(counts)) # → {'nonexistent': 0}
「存在確認のつもりでアクセスしたら、副作用でキーが増殖していた」というバグは頻出です。読み取り専用で触りたいときは defaultdict でも .get() を使うこと。d[key] と d.get(key) は意味が違います。
3-2. Counter:多重集合(マルチセット)としての辞書
要素の出現回数を数える専用マッピング。most_common() や算術演算まで備えます。
from collections import Counter
votes = Counter(["a", "b", "a", "c", "a", "b"])
votes.most_common(2) # → [('a', 3), ('b', 2)] 頻度順
votes["zzz"] # → 0 欠損キーは 0(KeyError を出さず、挿入もしない)
Counter("aab") + Counter("abc") # → Counter({'a': 3, 'b': 2, 'c': 1}) 加算
Counter("aab") & Counter("abc") # → Counter({'a': 1, 'b': 1}) 最小(積)
Counter の __missing__ は 0 を返すため、votes["zzz"] は defaultdict と違ってキーを増やしません。ランキング・出現頻度・在庫差分などで、手書きのカウントループを一掃できます。
3-3. OrderedDict:dict が順序を保つ今でも残る使い道
「dict は 3.7 から挿入順を保つのに、OrderedDict はもう要らないのでは?」——半分正解です。普通の用途では dict で十分。しかし OrderedDict だけが持つ機能があります。
from collections import OrderedDict
od = OrderedDict.fromkeys("abcd")
od.move_to_end("a") # 'a' を末尾へ(dict にはない)
od.popitem(last=False) # 先頭を取り出す → FIFO キューになる(dict は末尾固定)
# 等価性が「順序を区別する」
OrderedDict(a=1, b=2) == OrderedDict(b=2, a=1) # → False 順序まで比較
dict(a=1, b=2) == dict(b=2, a=1) # → True 順序は無視
move_to_end と popitem(last=False) は LRU キャッシュの構築に最適です(functools.lru_cache の内部発想)。「順序そのものが意味を持つ」場面では、いまも OrderedDict が正解です。
3-4. ChainMap:コピーせずに辞書を「重ねる」
複数のマッピングを 1 枚に見せ、先頭から順に検索します。設定の優先順位(CLI > 環境変数 > デフォルト)を、辞書をマージ(コピー)せずに表現できる——これが効きます。
from collections import ChainMap
defaults = {"theme": "light", "timeout": 30}
env = {"timeout": 60}
cli = {"theme": "dark"}
config = ChainMap(cli, env, defaults) # 先頭ほど優先
config["theme"] # → 'dark' (cli が勝つ)
config["timeout"] # → 60 (env が勝つ)
config["timeout"] # defaults の 30 は env に隠される
# 書き込み・削除は「先頭のマップだけ」に作用する
config["theme"] = "system"
cli # → {'theme': 'system'} ← defaults は不変のまま
辞書を {**defaults, **env, **cli} でマージするとメモリコピーが発生し、元の層を後から差し替えられません。ChainMap は層を保ったまま動的にオーバーレイできるため、設定管理やスコープ(変数の入れ子)に向きます。本格的な設定・シークレット管理は Pydantic Settings による設定管理 と組み合わせると堅牢です。
3-5. MappingProxyType:読み取り専用ビューで内部状態を安全に公開する
ここはセキュリティと API 設計に直結する、現場で差がつく知識です。クラスやモジュールの内部 dict をそのまま外へ返すと、呼び出し側に書き換えられて内部状態が壊れます。types.MappingProxyType は、元の dict への読み取り専用ビューを提供します。
from types import MappingProxyType
_internal = {"version": "1.0", "debug": False}
PUBLIC = MappingProxyType(_internal) # 読み取り専用の「窓」
PUBLIC["version"] # → '1.0'
PUBLIC["x"] = 1 # → TypeError: 'mappingproxy' object does not support item assignment
_internal["debug"] = True
PUBLIC["debug"] # → True ← コピーではなく「ビュー」。元の変更は反映される
「内部は可変、公開は不変」を 1 行で実現できます。dict(_internal) でコピーを返す方法もありますが、それはスナップショットであって、毎回コピーコストがかかり、元の更新も追えません。MappingProxyType はコピーなし・常に最新・書き換え不能——カプセル化の理想形です。実際、Python のクラスの __dict__ 属性もこの mappingproxy 型で公開されています。
4. 自作マッピングの設計:dict を継承してはいけない
要件が標準型に収まらないとき(大文字小文字を無視する、書き込み時に検証する、キーを正規化する…)、自分のマッピング型を作ります。ここで 9 割の人がやる間違いが、「dict を継承して __getitem__ を上書きする」ことです。
4-1. なぜ dict の直接継承は壊れるのか
# アンチパターン:dict を継承して __getitem__ を上書きしても…
class UpperDict(dict):
def __getitem__(self, key):
return super().__getitem__(key.upper())
d = UpperDict()
d["ABC"] = 1
d["abc"] # → 1 (__getitem__ は確かに効く)
d.get("abc") # → None ← get() は C 実装で、あなたの __getitem__ を呼ばない!
"abc" in d # → False ← __contains__ も迂回される
dict のメソッド(get / update / __contains__ / ** 展開など)は C で実装されており、内部であなたの Python の __getitem__ を経由しません。結果、「d["abc"] は効くのに d.get("abc") は効かない」という、デバッグ困難な半壊状態が生まれます。これは Python の有名な落とし穴です。
4-2. 正解その①:collections.abc.MutableMapping を継承する
5 つの抽象メソッドを実装し、get / update / __contains__ などはすべて ABC のミックスインに任せます。ミックスインはあなたの 5 メソッドを経由するので、振る舞いが完全に一貫します。
実例として、HTTP ヘッダのように大文字小文字を区別しないマッピングを作ります(requests ライブラリの CaseInsensitiveDict と同じ発想)。
from collections.abc import MutableMapping
from typing import Iterator
class CaseInsensitiveDict(MutableMapping):
"""大文字小文字を区別しないマッピング。元のキー表記は保持する。"""
def __init__(self, data: dict | None = None) -> None:
# 小文字キー → (元のキー表記, 値) を保持する内部 dict
self._store: dict[str, tuple[str, object]] = {}
if data:
self.update(data) # MutableMapping.update が __setitem__ を呼ぶ
def __setitem__(self, key: str, value: object) -> None:
self._store[key.lower()] = (key, value)
def __getitem__(self, key: str) -> object:
return self._store[key.lower()][1]
def __delitem__(self, key: str) -> None:
del self._store[key.lower()]
def __iter__(self) -> Iterator[str]:
return (original for original, _ in self._store.values())
def __len__(self) -> int:
return len(self._store)
headers = CaseInsensitiveDict({"Content-Type": "application/json"})
headers["content-type"] # → 'application/json' __getitem__
headers.get("CONTENT-TYPE") # → 'application/json' ミックスインが __getitem__ 経由!
"content-Type" in headers # → True __contains__ も一貫
list(headers) # → ['Content-Type'] 元の表記を保持
get も in も、すべて期待どおり大文字小文字を無視します。dict 継承の「半壊」とは対照的に、抽象メソッドを実装するだけで全 API が整合する——これが ABC を使う最大の理由です。
4-3. 正解その②:collections.UserDict(dict 寄りの簡便版)
「ほぼ dict のままで、一部だけ振る舞いを変えたい」なら UserDict が手軽です。内部に本物の dict(self.data)を持ち、メソッドが Python レベルで定義されているため、上書きが正しく合成されます。
from collections import UserDict
class LoggingDict(UserDict):
"""書き込みを記録する観測可能なマッピング(可観測性の最小例)。"""
def __setitem__(self, key, value):
print(f"[audit] set {key!r}") # 実務では structlog 等で構造化ログに
super().__setitem__(key, value)
⚠️
UserDictの注意点:UserDictは__contains__をself.dataに対して直接定義しています。そのため、CaseInsensitiveDictのようにキーの意味そのものを変える場合は、__getitem__だけでなく__contains__も上書きが必要です。キー変換を伴う複雑な自作はMutableMapping、軽い味付けはUserDict——と使い分けます。
4-4. 読み取り専用の自作マッピング
不変マッピングが欲しいだけなら、多くの場合 MappingProxyType(3-5)で十分です。値を遅延計算したい(アクセス時に動的に算出する)など、ロジックが必要なときだけ collections.abc.Mapping を継承し、__getitem__ / __iter__ / __len__ の 3 つを実装します。__setitem__ を実装しないので、構造的に書き換え不能になります。
5. マッピングをデータモデルに織り込む応用
5-1. 自作オブジェクトをキーにする:__hash__ / __eq__ の契約
マッピングのキーはハッシュ可能でなければなりません。自作クラスをキーにするには、__hash__ と __eq__ を一貫して定義する必要があります。鉄則は「等しいオブジェクトは等しいハッシュ値を持つ」こと。これを破ると、辞書から値を取り出せなくなります。
最も安全な定石は @dataclass(frozen=True) です。__eq__ と __hash__ を矛盾なく自動生成してくれます。
from dataclasses import dataclass
@dataclass(frozen=True) # frozen=True で __hash__ が自動生成される
class GeoPoint:
lat: float
lng: float
cities = {GeoPoint(35.68, 139.69): "Tokyo"}
cities[GeoPoint(35.68, 139.69)] # → 'Tokyo' 値が等しければ同じキー
# 罠:eq だけ定義し frozen にしないと、__hash__ が None にされる
@dataclass # eq=True(既定), frozen=False(既定)
class Mutable:
x: int
{Mutable(1): "v"} # → TypeError: unhashable type: 'Mutable'
「__eq__ を定義すると __hash__ が無効化される」——これは「中身が変わりうるオブジェクトをキーにすると、ハッシュ位置がずれて壊れる」という Python の安全装置です。キーにするなら不変(frozen)にする、という設計が正しい理由がここにあります。
5-2. 構造的パターンマッチでマッピングを分解する(3.10+)
match 文はマッピングに対しても使え、「特定のキーを持つか」で分岐し、値を取り出すことができます。JSON イベントやコマンドのディスパッチを、ネストした if より遥かに読みやすく書けます。
def handle(event: dict) -> str:
match event:
case {"type": "click", "x": int(x), "y": int(y)}:
return f"click at ({x}, {y})"
case {"type": "key", "code": str(code)}:
return f"key {code}"
case {"type": str(kind), **rest}: # 残りのキーを rest で受ける
return f"unhandled {kind} with {rest}"
case _:
return "not an event"
handle({"type": "click", "x": 10, "y": 20}) # → 'click at (10, 20)'
マッピングパターンは部分一致です(余分なキーがあってもマッチする)。int(x) のように型でガードしつつ束縛できるため、外部入力の安全な分解にも使えます。
5-3. 「stringly-typed な dict」をやめる判断
dict は強力ですが、何でも dict[str, Any] で持ち回るのは技術的負債です。user["emial"](タイポ)は実行時まで露見せず、IDE 補完も型チェックも効きません。「形が決まっているデータ」は、dict から型のあるモデルへ昇格させます。
# アンチパターン:意味のある構造を生 dict で持つ
def total_price(order: dict) -> float:
return order["price"] * order["quantity"] # キー名はタイポし放題、型も不明
# 改善:dataclass で「形」を型にする
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Order:
price: int # 最小通貨単位(cent)で持つ
quantity: int
def total_price(order: Order) -> int:
return order.price * order.quantity # 補完が効き、タイポは静的に落ちる
同じ思想を TypeScript で突き詰めた規律は TypeScript 型安全の規律(Zod・NeverError・no-any) にまとめています。言語は違えど「データに形(型)を与え、不正な状態を表現不能にする」原則は共通です。
6. 本番の落とし穴:マッピングで実際に起きる事故
6-1. 反復中の変更で RuntimeError
dict を反復しながらサイズを変えると、実行時に落ちます。
d = {"a": 1, "b": 2, "c": 3}
for k in d:
if d[k] == 2:
del d[k] # → RuntimeError: dictionary changed size during iteration
# 正解:反復対象のスナップショットを固定する
for k in list(d): # キーのリストを先に作る
if d[k] == 2:
del d[k]
6-2. 可変な dict を「共有」してしまう
関数のデフォルト引数に dict を置く、クラス属性に dict を置く——いずれもインスタンス間で共有され、片方の変更が全体に漏れます。デフォルトは None センチネルで受けて関数内で生成し、クラス属性は field(default_factory=dict) を使います(詳細は前作 Python のデータ型 完全ガイド のミュータブルデフォルト引数を参照)。
6-3. スレッド安全性を GIL に頼らない
「CPython の GIL があるから dict 操作はスレッドセーフ」という俗説は危険です。d[k] = v のような単一操作は原子的でも、「確認してから代入」のような複合操作は原子的ではありません。
# 非原子的:チェックと代入の間に別スレッドが割り込みうる
if key not in counters:
counters[key] = 0
counters[key] += 1 # 読み出し → 加算 → 書き戻しも非原子的(更新が消える)
しかも、フリースレッド版 CPython(PEP 703、3.13 で実験導入)では GIL 由来の暗黙の保護すら消えます。共有マッピングへの複合操作は、明示的な threading.Lock で保護する——これが移植性のある正解です。並行処理の信頼性設計は リトライ・バックオフ・サーキットブレーカー などの回復性パターンと併せて設計します。
7. 最重要:外部から来る dict を信頼しない(境界での型検証)
ここまでの知識を本番アーキテクチャに接続します。Web API のリクエストボディ、外部 API のレスポンス、設定ファイル——これらを json.loads() でパースした結果は、型の保証が一切ない、ただの dict[str, Any] です。これを検証せずに内側へ通すと、KeyError や TypeError、最悪の場合は不正なデータによるセキュリティ事故になります。
import json
raw = json.loads('{"id": 1, "name": "友田"}') # ← 型は dict[str, Any]。中身は無保証
TypedDict は「dict の形」を静的な型注釈として表現できますが、実行時には検証しません(注釈にすぎない)。実行時に「本当にこの形か」を保証するには、Pydantic / marshmallow を使います。
from pydantic import BaseModel, EmailStr, Field
class CreateUser(BaseModel):
id: int
name: str = Field(min_length=1, max_length=50)
email: EmailStr | None = None
# 外部の dict を検証して、信頼できる型に変える。不正なら ValidationError で堰き止める
user = CreateUser.model_validate(raw)
**マッピング(dict)はシステム境界の「共通通貨」**です。だからこそ、境界では必ず検証して、内側へは「検証済みの型」だけを通す——これがセキュアで壊れないバックエンドの第一原則です。
- 型ファーストの境界検証 → Pydantic v2 実践ガイド
- ORM / フレームワーク非依存のシリアライズ/検証 → marshmallow 実践ガイド
- Web フレームワーク層での入力検証 → FastAPI 本番運用ガイド と FastAPI のリクエスト検証
- LLM の JSON 出力をスキーマ検証する → LLM 構造化出力の検証
まとめ:マッピングを「設計の道具」として持つ
Python のマッピングは、dict という一つの型ではなく、「キー → 値の対応」という抽象と、その豊富な実装群、そして自作する能力の総体です。要点を判断軸として持ち帰ってください。
- マッピングはプロトコル。
Mapping(読み取り専用)/MutableMapping(読み書き)を型で要求すれば、契約が明確になる。 - 目的に合った実装を選ぶ。集計は
Counter、グルーピングはdefaultdict、設定の階層化はChainMap、安全な公開はMappingProxyType。 dictを直接継承しない。自作はMutableMapping(キー意味を変えるなら)かUserDict(軽い味付け)。- キーにする型は
__hash__/__eq__の契約を守る。@dataclass(frozen=True)が安全な定石。 - 境界の dict は信頼しない。
TypedDictは静的注釈にすぎず、実行時検証は Pydantic / marshmallow で行う。
dict を「使う」だけなら誰でもできます。マッピングを「設計する」と、コードは宣言的になり、不正な状態が表現不能になり、変更に強くなります。 筆者は、この発想を経済産業大臣賞を受賞した B2B SaaS のバックエンドで実践し、Router → UseCase → Repository の各層でデータの形を型として設計してきました。「データに形を与える」ことこそ、テストをすり抜けるバグを未然に消し、保守性と拡張性を最大化する近道です。
よくある質問(FAQ)
Q. dict と「マッピング」は何が違うの?
dict は「マッピング(キー → 値の対応)」という抽象の、最も高速で一般的な実装の一つです。defaultdict / Counter / ChainMap / MappingProxyType や自作クラスもマッピングです。関数の引数で collections.abc.Mapping を要求すれば、dict に限定せず「マッピングらしいもの」全般を受け取れます。
Q. defaultdict と dict.setdefault() はどちらを使うべき?
繰り返しグルーピング・集計するなら defaultdict が宣言的で読みやすいです。単発の「なければ初期化」なら dict.setdefault() で十分。ただし defaultdict はアクセスしただけでキーを作るため、読み取り目的のときは .get() を使い分けてください。
Q. dict が順序を保つ今、OrderedDict は不要では?
普通の用途では dict で十分です。ただし move_to_end() / popitem(last=False)(FIFO)や、順序を区別する等価比較が必要なら、いまも OrderedDict が正解です。LRU キャッシュの実装などで重宝します。
Q. 自作の辞書を作りたい。dict を継承していい?
避けてください。dict のメソッド(get / update / in など)は C 実装で、あなたが上書きした __getitem__ を呼ばないため、振る舞いが半分しか変わらず壊れます。collections.UserDict(軽い変更)か collections.abc.MutableMapping(キーの意味ごと変える)を継承するのが正解です。
Q. JSON をパースした dict は、そのまま使っていい?
いいえ。json.loads() の結果は型保証のない dict[str, Any] です。境界では Pydantic / marshmallow で実行時検証し、id は整数か、必須キーはあるか、を確認してから内側へ通します。TypedDict は静的な型注釈にすぎず、実行時には検証しない点に注意してください。