Q1次のうちミュータブルな型だけを集めたものはどれですか?
ミュータブルとイミュータブル
Python のミュータブル(変更可能)とイミュータブル(変更不可)の違いを解説します。参照の共有で起きるバグと copy() での回避まで、一通り図解で押さえます。
なぜこれを学ぶ必要があるのか
この記事は完全な初心者向けではありませんが、プログラミングを深く理解するには避けて通れない内容です。初心者がプログラミングを学ぶ中で、ミュータブルのバグに遭遇してデバッグに何時間も費やすのはよくある話です。
Python では、y = x のように変数をイコールでつないだだけのつもりが、片方を編集するともう片方まで書き換わってしまうことがあります。これがミュータブルです。
この章では 「ミュータブル」(変更可能) と 「イミュータブル」(変更不可能) の違いを押さえ、安全にコピーする方法まで身につけます。
変数そのものを書き換える系の操作(append、要素代入、update など)ができるのがミュータブル、できないのがイミュータブルです。
list / dict / set がミュータブル、それ以外(int / float / str / bool / tuple)はイミュータブルです。
イミュータブル型 — 片方を変えても影響しない
イミュータブルな型(変更不可能な型)は、y = x で値を渡したあとに x を変更しても、y には何も影響しません。
例えば、x += 1 とすると、x は 11 になりますが、y は変わらず元の 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')
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 側からも同じ変化が見えてしまいます。
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 では 変数名は値そのものではなく、メモリ上の場所を指す名札 になっています。
この仕組み自体は変えられないので、こちら側(書き手)が気を付けて使い分ける必要があります。
copy() で別物として扱う
「元のデータを残したまま、別バージョンを作りたい」ときは、copy() メソッドを使います。
y = x.copy() と書くと、中身を新しい箱にまるごと写してから y に渡すため、その後 x を書き換えても y には影響しなくなります。
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() で別物として扱う方法を学びました。
「変数名は中身そのものではなく、メモリ上の場所を指す名札」という考え方は、Python だけでなく他の言語にも共通です。ミュータブルな型を扱うときは copy() を 1 つ挟みましょう。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2次のコードを実行したあとの b の値はどれですか?
a = [1, 2, 3]
b = a
a.append(4)
Q3元のリスト a の中身を残したまま、独立した別バージョン b を作りたいとき、最も適切な書き方はどれですか?