Pythonの型とミュータブル/イミュータブル完全ガイド
はじめに
- Python では 「型」(type) と「ミュータビリティ」(mutability) が実行時の挙動を左右する。
- ミュータブルかイミュータブルかで、関数引数の副作用・スレッドセーフ設計・メモリ効率が変わる。
- 本記事では標準型を軸に、設計で迷わないための実践的ノウハウを整理する。
1. Python 型システムの全体像
1‑1. 数値系 (Numeric)
-
int: 任意精度整数。イミュータブル。 -
float: IEEE 754 倍精度。イミュータブル。 -
complex: 実部・虚部を持つ複素数。イミュータブル。
1‑2. シーケンス系 (Sequence)
-
list: 可変長配列。ミュータブル。 -
tuple: 不変タプル。イミュータブル。 -
range: 遅延シーケンス。イミュータブル。 -
str,bytes: 文字列/バイト列。イミュータブル。 -
bytearray: ミュータブルなバイト列。
1‑3. マッピング系 (Mapping)
-
dict: ハッシュマップ。ミュータブル。 -
types.MappingProxyType: 読み取り専用ラッパー。イミュータブル風味。
1‑4. 集合系 (Set)
-
set: ハッシュ集合。ミュータブル。 -
frozenset: 不変集合。イミュータブル。
1‑5. ユーザー定義
- クラス/
dataclass/NamedTupleなど。__slots__とfrozen=Trueでイミュータブル化可能。
2. ミュータブル vs イミュータブル を見分ける
2‑1. 定義
- ミュータブル: インスタンス生成後に 内部状態を変えられる。
- イミュータブル: 変更不可。演算すると 新しいオブジェクト が返る。
2‑2. 標準型の分類早見表
- ✅ ミュータブル:
list,dict,set,bytearray, カスタムクラス(デフォルト) - ❌ イミュータブル:
int,float,complex,str,bytes,tuple,frozenset,range
2‑3. “見かけ倒し”に注意
-
tupleはイミュータブルだが、内部にミュータブル要素を持つと全体としては書き換え可能。t = ([],) t[0].append(1) # タプル自体は変更せず、中のリストを書き換え -
C 拡張で実装された型 (
numpy.ndarrayなど) は独自のミュータビリティルールを持つ。
3. 関数呼び出しと副作用
3‑1. Python の引数評価モデル
- Pass‑by‑object‑reference: 参照(オブジェクトのアドレス値)を丸ごとコピー。
- ミュータブルなら同じオブジェクトを共有、イミュータブルなら新オブジェクト生成が起こりうる。
def push(lst, x):
lst.append(x) # ミュータブル → 呼び出し元に影響
def add_one(n):
n += 1 # イミュータブル → 元は不変
3‑2. 破壊的操作 vs 非破壊的操作
- リスト操作の例:
list.sort()は破壊的 (in‑place)、sorted(list)は非破壊的。 - API 設計時は 命名で意図を明示 (例:
update_prefix を付ける)。
3‑3. デフォルト引数の罠
def bad_append(elem, lst=[]):
lst.append(elem)
return lst
bad_append(1) # -> [1]
bad_append(2) # -> [1, 2] ← 意図せず共有
- デフォルト値は 関数定義時に一度だけ評価され、ミュータブルだと全呼び出しで共有される。
- 対策:
Nonesentinel パターン。
4. 実務パターン
4‑1. 不変型で安全に受け渡す
- マルチスレッドや asyncio で共有データを扱うときは
tuple,frozenset,types.MappingProxyTypeで防御的コピー。
4‑2. dataclass(frozen=True) で DTO を定義
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
- イミュータブルなので
hash()可能。辞書キーやセット要素に安全。
4‑3. コピー戦略の選択
| シナリオ | 推奨手段 |
|---|---|
| 浅い階層で十分 | copy.copy() |
| ネスト構造をまるごと複製 | copy.deepcopy() |
| 大規模配列/画像 |
numpy.copy() or tensor.clone()
|
4‑4. メモリ節約テクニック
-
__slots__で属性辞書を削除し、1 インスタンスあたり数十バイト削減。 - イミュータブル化すると GC の追跡対象外になり、トレース負荷が低減。
5. テスト・デバッグの観点
- Stateful テスト: ミュータブルな戻り値はテスト後に初期化しておく。
-
参照カウント確認:
sys.getrefcount(obj)で意図しない循環参照を検出。 -
メモリプロファイル:
tracemallocでスナップショットを比較し、コピー回数過多をあぶり出す。
6. ベストプラクティスまとめ
- ミュータブルを渡すときは副作用をドキュメント。
- 変更より生成を優先: Functional プログラミングライクなコードはバグ率が下がる。
- 巨大データ構造は copy-on-write 方針: PyArrow や pandas はスライスをコピーせず共有する設計。
- イミュータブル型をハッシュキーに使うのが鉄則。
- C 拡張や外部ライブラリの型は公式ドキュメントで mutability を必ず確認。
まとめ
Python では「参照のコピー」が基礎ルールだが、ミュータビリティの有無で副作用が生じるかどうかが決まる。設計初期にデータのライフサイクルを決め、mutable と immutable を使い分ければ、安全で高速なコードベースを維持できる。