Python | データ構造強化:mutable / immutable

Python Python
スポンサーリンク
  1. 概要(mutable と immutable は「変更できるかどうか」を決める重要な性質)
  2. 基本の分類と直感(ここが重要)
    1. 代表的な mutable と immutable の区別
    2. id() で“同じものか”を確認する直感法
  3. 代入・コピー・参照共有(バグの温床を潰す)
    1. 「= はコピーではなく“同じものを指す”」を体に入れる
    2. シャローコピーとディープコピーの違い(階層に気づく)
  4. 関数引数・デフォルト引数の挙動(重要部分は深掘り)
    1. 引数で渡した mutable は“中身の変更が外へ効く”
    2. デフォルト引数に mutable を使う落とし穴
  5. 不変の利点と例外的ケース(パフォーマンス・安全・中に mutable)
    1. immutable のメリット(安全性・ハッシュ可能・高速化)
    2. “immutable の中に mutable”は変わり得る
  6. 実務の選び方と最適化(要件に応じて性質を使い分ける)
    1. “変更が必要か、共有したいか、キーにしたいか”で決める
    2. 参照共有で効かせる/遮断するを意図的に設計する
    3. immutability を型で担保する選択肢
  7. 例題で身につける(定番から一歩先まで)
    1. 例題1:mutable と immutable の“変更”の違いを体感
    2. 例題2:参照共有による“連動バグ”と防御的コピー
    3. 例題3:デフォルト引数の罠を回避
    4. 例題4:辞書キーに使える不変構造と使えない構造
    5. 例題5:文字列結合の最適化(join へ寄せる)
  8. まとめ

概要(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] に変わる
Python

immutable は“中身”が変えられないので、代入しても共有問題が表面化しにくいです。ただし再代入は「新しい別のオブジェクト」を指す動きになります。

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](汚さない)
Python

immutable を渡した場合、関数内で“変更”しても新しい値を返すだけなので、元は変わりません。

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 等)も活用しつつ、“共有する/遮断する”を設計で明示すれば、短く、速く、壊れないコードにまとまります。

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