順番に読み進めながら学べます

ミュータブルとイミュータブル

Python のミュータブル(変更可能)とイミュータブル(変更不可)の違いを解説します。参照の共有で起きるバグと copy() での回避まで、一通り図解で押さえます。

なぜこれを学ぶ必要があるのか

この記事は完全な初心者向けではありませんが、プログラミングを深く理解するには避けて通れない内容です。初心者がプログラミングを学ぶ中で、ミュータブルのバグに遭遇してデバッグに何時間も費やすのはよくある話です。

Python では、y = x のように変数をイコールでつないだだけのつもりが、片方を編集するともう片方まで書き換わってしまうことがあります。これがミュータブルです。

この章では 「ミュータブル」(変更可能)「イミュータブル」(変更不可能) の違いを押さえ、安全にコピーする方法まで身につけます。

型の分類 — どちらに属するか
イミュータブルint, floatstr, bool, tuple片方を変えても独立ミュータブルlist, dictset片方を変えると両方変わる属する特徴属する特徴

変数そのものを書き換える系の操作append、要素代入、update など)ができるのがミュータブル、できないのがイミュータブルです。

list / dict / set がミュータブル、それ以外(int / float / str / bool / tuple)はイミュータブルです。

イミュータブル型 — 片方を変えても影響しない

イミュータブルな型(変更不可能な型)は、y = x で値を渡したあとに x を変更しても、y には何も影響しません

例えば、x += 1 とすると、x11 になりますが、y は変わらず元の 10 のままです。

イミュータブルな代入の流れ
x = 10y = xx += 1x = 11y = 10実行結果
# 整数 (int) はイミュータブル
x = 10
y = x
x += 1
print(x)   # 11
print(y)   # 10  ← 元のまま

# 文字列 (str) もイミュータブル
x = "hello"
y = x
x = x + " world"
print(x)   # hello world
print(y)   # hello

# タプル (tuple) もイミュータブル
x = ("a", "b")
y = x
x = ("c", "d")
print(x)   # ('c', 'd')
print(y)   # ('a', 'b')

イミュータブルな型では、値を他の変数に入れたあとに元の変数を更新しても、コピー側は影響を受けません

① 商品の価格 price = 1000 を別の変数 old_price にコピーし、price = price + 500 で値上げします。priceold_priceprint() で表示してください。

② タグのタプル tags = ("sale", "new")old_tags にコピーし、tags = ("limited", "gift") で別のタプルに付け替えます。tagsold_tags を表示してください。

(正しく実行できれば解説が表示されます)

Python エディタ

コードを実行してください

Python の変数 = 「箱への名札」

Python の変数は、値そのものを持っているわけではなく、メモリ上のデータ()を指す名札のようなものです。y = x と書くと、新しい箱を作るのではなく、x が指している同じ箱に y も名札を貼る 動きになります。

- イミュータブル型: x = 11 のような再代入は、x の名札を別の箱に貼り替える だけ。y は元の箱を指し続けるので 独立した値のまま

- ミュータブル型: x.append(...) のような 中身を書き換える操作 は、共有された箱そのもの を変えるので y 側にも変化が見える

これがイミュータブルとミュータブルの分かれ目です。

ミュータブル型 — y = x で片方を変えるともう片方も変わる

ところがミュータブルな型(list / dict / set)では、y = x と書くと y と x は同じ箱を指す 状態になります。

この状態で x.append(...) のように中身を書き換える操作をすると、y 側からも同じ変化が見えてしまいます

ミュータブルな代入の流れ — 同じ箱を共有
x=[a,b]y=xx.append(c)x=[a,b,c]y=[a,b,c]実行両方

y = x で別々のコピーが作られるわけではなく、差し先は同じ場所になります

中身を書き換える append のような操作は、両方の名札が指す共通の箱を変えるので、y 側からも変化が見えることになります。

# list はミュータブル
x = ["a", "b"]
y = x          # 同じリストに名札 y を追加で貼っただけ

x.append("c")  # 中身を直接書き換える
print(x)       # ['a', 'b', 'c']
print(y)       # ['a', 'b', 'c']  ← y 側も増えている!

# remove も同じ
x.remove("b")
print(y)       # ['a', 'c']  ← y 側からも消える

# dict と set でも同じ現象が起きる
d = {"k": 1}
e = d
e["new"] = 99
print(d)       # {'k': 1, 'new': 99}  ← d 側も増えている

なぜわざわざ「共有」する仕組みになっているのか

もし y = x のたびに毎回中身を丸ごと複製していたら、例えばxがデータベースから数百万件のレコードを取得した値だった場合、メモリも処理時間もどんどん膨らんでしまいます

そのため Python では 変数名は値そのものではなく、メモリ上の場所を指す名札 になっています。

この仕組み自体は変えられないので、こちら側(書き手)が気を付けて使い分ける必要があります。

ミュータブル型で起きる共有を確認しましょう。

① 買い物カート cart = ["牛乳", "パン"] を作り、old_cart = cart としてください(コピーのつもりで)。

cart.append("卵")cart だけに "卵" を追加したつもりで、cartold_cart をそれぞれ print() で表示してください。

両方に "卵" が入っているはずです。これが今回の落とし穴です。

Python エディタ

コードを実行してください

copy() で別物として扱う

元のデータを残したまま、別バージョンを作りたい」ときは、copy() メソッドを使います。

y = x.copy() と書くと、中身を新しい箱にまるごと写してから y に渡すため、その後 x を書き換えても y には影響しなくなります。

copy() で「別の箱」を作る
x=[a,b]y=x.copy()x.append(c)x=[a,b,c]y=[a,b]実行独立

y = x.copy() の時点で別のメモリ領域が作られます

そのため、x.append(...) などで x の中身を変えても y には何も影響しません

# list / dict / set すべてに .copy() がある
x = ["apple", "lemon"]
y = x.copy()

x.append("grape")
print(x)   # ['apple', 'lemon', 'grape']
print(y)   # ['apple', 'lemon']  ← 影響なし

# dict.copy() も同じ
d = {"a": 1, "b": 2}
e = d.copy()
d["c"] = 3
print(d)   # {'a': 1, 'b': 2, 'c': 3}
print(e)   # {'a': 1, 'b': 2}  ← 影響なし

# set.copy() も同じ
s = {1, 2}
t = s.copy()
s.add(3)
print(s)   # {1, 2, 3}
print(t)   # {1, 2}            ← 影響なし

# list は他にもコピーの書き方がある
x = ["apple", "lemon"]
y1 = list(x)   # 関数で変換しても新しいリストになる
y2 = x[:]      # スライスで全コピー(先頭から末尾まで)

リストの中にリストを入れている場合は要注意

copy()「外側の箱だけ」 を新しく作ります。中に入っているミュータブルな値(リストの中のリストなど)は依然として共有されたままです。これを「浅いコピー」(shallow copy)と呼びます。

入れ子になったデータを扱うときは、この点に注意してください。

前の節で起きたバグを copy() で修正しましょう。

cart = ["牛乳", "パン"] を作り、今度は old_cart = cart.copy() としてください。cart.append("卵") を実行し、cartold_cartprint() で表示します。old_cart には "卵" が入らないはずです。

Python エディタ

コードを実行してください

この記事では、ミュータブルとイミュータブルの違い、そして copy() で別物として扱う方法を学びました。

変数名は中身そのものではなく、メモリ上の場所を指す名札」という考え方は、Python だけでなく他の言語にも共通です。ミュータブルな型を扱うときは copy() を 1 つ挟みましょう。

QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1次のうちミュータブルな型だけを集めたものはどれですか?

Q2次のコードを実行したあとの b の値はどれですか?


a = [1, 2, 3]
b = a
a.append(4)

Q3元のリスト a の中身を残したまま、独立した別バージョン b を作りたいとき、最も適切な書き方はどれですか?