概要(mutable と immutable は「変更できるかどうか」を決める重要な性質)
Python のオブジェクトは、作成後に中身を変更できるもの(mutable)と、変更できないもの(immutable)に分かれます。これを理解すると「代入で別物になるのか」「同じものを指したまま中身が変わるのか」「関数に渡した後に外側へ影響するのか」が見通せるようになり、コピー・デフォルト引数・パフォーマンスの落とし穴を避けられます。
基本の分類と直感(ここが重要)
代表的な mutable と immutable の区別
- mutable(変更可能):list、dict、set、bytearray、deque(外部ライブラリ含む)
- immutable(変更不可):int、float、bool、str、tuple、frozenset、bytes
“変更不可”は「再代入はできるが、中身を直接書き換えられない」という意味です。immutable を変える操作は「新しいオブジェクトを作って差し替える」動きになります。
id() で“同じものか”を確認する直感法
id() は「オブジェクトの一意な識別子」。mutable を変更しても id は同じ、immutable を“変更”すると新しいオブジェクトになり id が変わることが多いです。
# mutable: list の変更は“同じ入れ物の中身が変わる”
a = [1, 2]
print(id(a))
a.append(3)
print(id(a)) # 同じ
# immutable: str の“変更”は新しい文字列へ差し替え
s = "hi"
print(id(s))
s = s + "!"
print(id(s)) # 別になる(新規)
Python代入・コピー・参照共有(バグの温床を潰す)
「= はコピーではなく“同じものを指す”」を体に入れる
代入は別名を作るだけ。mutable は片方の変更がもう片方にも反映します。
x = [1, 2]
y = x # 同じリストを指す
y[0] = 99
print(x) # [99, 2] に変わる
Pythonimmutable は“中身”が変えられないので、代入しても共有問題が表面化しにくいです。ただし再代入は「新しい別のオブジェクト」を指す動きになります。
a = 10
b = a
b = b + 1
print(a, b) # 10 11(a は影響なし)
Pythonシャローコピーとディープコピーの違い(階層に気づく)
入れ子の mutable を持つとき、外側だけのコピー(シャロー)は中身の参照を共有します。完全に独立させたいならディープコピーが必要です。
import copy
a = [[1], [2]]
b = a.copy() # 外側だけ新規、内側は共有
b[0].append(9)
print(a) # [[1, 9], [2]]
c = copy.deepcopy(a) # 全階層を独立
c[1].append(8)
print(a) # [[1, 9], [2]]
print(c) # [[1, 9], [2, 8]]
Python“必要な階層だけ防御的にコピーする”のが現場では最適解です(コストを抑えつつ副作用を回避)。
関数引数・デフォルト引数の挙動(重要部分は深掘り)
引数で渡した mutable は“中身の変更が外へ効く”
関数内でリストや辞書を破壊的に変更すると、呼び出し元のデータに影響します。意図的でないなら、最初にコピーしてから操作します。
def add_mark(xs):
ys = xs[:] # 防御的コピー(一次階層)
ys.append("OK")
return ys
original = [1, 2]
print(add_mark(original)) # [1, 2, 'OK']
print(original) # [1, 2](汚さない)
Pythonimmutable を渡した場合、関数内で“変更”しても新しい値を返すだけなので、元は変わりません。
def inc(n): return n + 1
v = 10
print(inc(v), v) # 11 10
Pythonデフォルト引数に mutable を使う落とし穴
関数定義時に一度だけ作られたリストや辞書が“共有”され続けます。回避は「None をデフォルトにして中で新規作成」。
def append_bad(x, buf=[]): # NG: buf を共有してしまう
buf.append(x)
return buf
print(append_bad(1)) # [1]
print(append_bad(2)) # [1, 2] (前回のが残る)
def append_good(x, buf=None): # OK: 呼び出しごとに作る
if buf is None:
buf = []
buf.append(x)
return buf
Python不変の利点と例外的ケース(パフォーマンス・安全・中に mutable)
immutable のメリット(安全性・ハッシュ可能・高速化)
- 共有しても中身が変わらないため、安全にキャッシュや辞書キーに使える(tuple、str、frozenset、bytes など)。
- 変更不能ゆえにコンパクトな実装になりやすく、比較やコピーが速い場面がある。
key = ("user", 101) # タプルは辞書キーにできる
index = {key: "alice"}
Python文字列の“足し算”を大量にすると新規生成でコストがかさむため、join に寄せるのが定石です(immutable の特性を踏まえた最適化)。
parts = ["a", "b", "c"]
print("".join(parts)) # 速くてメモリ効率が良い
Python“immutable の中に mutable”は変わり得る
tuple は不変ですが、「中にリストが入っている」場合、そのリストは変えられてしまいます。外側が不変でも内側のミュータブルに注意。
t = (1, [2, 3])
t[1].append(4)
print(t) # (1, [2, 3, 4])
Python完全不変が必要なら、内側も immutable(例:list→tuple、set→frozenset)に揃えます。
実務の選び方と最適化(要件に応じて性質を使い分ける)
“変更が必要か、共有したいか、キーにしたいか”で決める
- しばらく固定で安全に共有したい、キーに使いたい → immutable(tuple、frozenset、bytes、str)
- 中身を更新しながら積み上げたい、順序を保持したい → mutable(list、dict、set、deque)
- 外へ影響を絶対に出したくない加工 → 必要階層だけコピーしてから操作(防御的コピー)
参照共有で効かせる/遮断するを意図的に設計する
巨大データの読み取りだけなら共有(コピーしない)で性能を稼ぎ、書き込みがある経路は防御的コピーで被害範囲を限定します。関数のドキュメンテーションに「破壊的変更をする/しない」を明記すると保守性が上がります。
immutability を型で担保する選択肢
使用頻度が高い構造なら、namedtuple や dataclasses.dataclass(frozen=True) で「意図的な不変レコード」を導入すると事故が減ります。
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
p = Point(1, 2)
# p.x = 3 # FrozenInstanceError(変更不可)
Python例題で身につける(定番から一歩先まで)
例題1:mutable と immutable の“変更”の違いを体感
nums = [1, 2]
nums.append(3) # 中身を変更(同じオブジェクト)
print(nums)
text = "ab"
text = text + "c" # 新しい文字列へ差し替え
print(text)
Python例題2:参照共有による“連動バグ”と防御的コピー
catalog = {"coffee": ["latte", "espresso"], "tea": ["earl"]}
shallow = catalog.copy() # 外側だけ新規、値は共有
shallow["coffee"].append("mocha")
print(catalog["coffee"]) # 連動して増える
safe = {k: v[:] for k, v in catalog.items()} # 値までコピー
safe["coffee"].append("cappuccino")
print(catalog["coffee"]) # 影響しない
Python例題3:デフォルト引数の罠を回避
def push_bad(x, buf=[]): # 共有されてしまう
buf.append(x)
return buf
print(push_bad(1))
print(push_bad(2)) # 前回の 1 が残る
def push_good(x, buf=None):
if buf is None:
buf = []
buf.append(x)
return buf
Python例題4:辞書キーに使える不変構造と使えない構造
ok_key = (1, 2) # タプルは OK
bad_key = [1, 2] # リストは NG(ハッシュ不可)
d = {ok_key: "yes"}
# d[bad_key] = "no" # TypeError
Python例題5:文字列結合の最適化(join へ寄せる)
parts = [str(i) for i in range(5)]
print("".join(parts)) # '01234'
Pythonまとめ
mutable/immutable の違いは「変更が中身へ直接及ぶか、新しいオブジェクトへ差し替えるか」を決める根本概念です。代入は“同じものを指す”だけ、mutable の共有は連動バグの原因、入れ子構造ではコピーの階層を意識する。関数引数は mutable を破壊的に触ると外へ効くため、意図がないなら防御的コピー。デフォルト引数に mutable を置かない。安全性やキー利用には immutable を選び、完全不変が必要なら中まで不変に揃える。不変レコード(frozen dataclass 等)も活用しつつ、“共有する/遮断する”を設計で明示すれば、短く、速く、壊れないコードにまとまります。
