Pythonのミュータブル・イミュータブルとは?|型ごとの違いを整理

ソフトウェア

はじめに

Pythonを勉強していると、「ミュータブル(mutable)」と「イミュータブル(immutable)」という言葉に出会うことがあります。名前だけを見ると少し難しそうですが、ポイントはそれほど複雑ではありません。

ミュータブル:同じオブジェクトのまま内容を変更できる
イミュータブル:作成されたオブジェクトの内容を変更できない

たとえば、Pythonの list はミュータブルです。

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

print(numbers)

実行結果は次のようになります。

[1, 2, 3, 4]

numbers が参照している同じリストオブジェクトに、4 が追加されています。一方、int や str はイミュータブルで、一度作られたオブジェクトの内容そのものを変更することはできません。

この違いは、単なる型の分類ではありません。「なぜリストは別の変数からも変更されるのか」「なぜ文字列の一部を書き換えられないのか」といったPython特有の動きを理解するうえで重要です。

この記事では、Pythonでよく使う型を比較しながら、ミュータブルとイミュータブルの違いを整理していきます。

この記事でわかること

この記事では、次の内容の理解を目指します。

  • ミュータブルとイミュータブルの意味
  • Pythonの代表的な型がどちらに分類されるのか
  • int や str がイミュータブルであるとはどういうことか
  • list や dict がミュータブルであるとはどういうことか
  • tuple がイミュータブルでも注意が必要な理由
  • ミュータブル・イミュータブルと「変数の参照」の関係

1. ミュータブルとイミュータブルとは?

まずは、言葉の意味から確認しておきましょう。ミュータブル(mutable)には「変更可能」、イミュータブル(immutable)には「変更できない」という意味があります。

Pythonでは、変数そのものではなく、変数名が参照しているオブジェクトが変更できるかどうかを考えます。

ミュータブルとイミュータブルを判断するときは、「そのオブジェクト自身の内容を変更できるか?」と考えるのがポイントです。

たとえば、list は同じリストオブジェクトのまま要素を追加したり削除したりできるため、ミュータブルです。一方、int や str は、作成されたオブジェクトそのものの内容を変更できないため、イミュータブルです。

この違いを理解するには、前回の記事で説明した「変数名とオブジェクトを分けて考える」という考え方が重要になります。

▶ Pythonの変数・オブジェクト・参照について詳しく知りたい
Pythonの変数は「箱」ではない?|オブジェクトと参照を理解する
Pythonの変数は本当に「値を入れる箱」なのでしょうか。オブジェクトと参照の仕組みを図解しながら、代入や再代入、ミュータブルとイミュータブル、コピー、参照カウントまでわかりやすく解説します。

2. Pythonの型を一覧で整理

まずは、Pythonでよく使う代表的な型を一覧で整理してみましょう。

型 分類 代表的な変更操作
bool イミュータブル ―
int イミュータブル ―
float イミュータブル ―
complex イミュータブル ―
str イミュータブル ―
tuple イミュータブル ―
bytes イミュータブル ―
list ミュータブル append()、要素の代入など
dict ミュータブル キーへの代入など
set ミュータブル add()、remove() など
bytearray ミュータブル 要素の代入など

代表的な型だけを大きく分けると、list・dict・set などはミュータブルです。一方、数値を扱う int・float・complex・bool や、str・tuple・bytes などはイミュータブルです。

代表的なミュータブル:list、dict、set、bytearray
代表的なイミュータブル:bool、int、float、complex、str、tuple、bytes

ただし、この一覧を暗記するだけでは、ミュータブルとイミュータブルを理解したことにはなりません。大切なのは、この違いによってPythonのコードの動きがどう変わるのかです。

ここから、代表的な型を実際のコードで比べてみましょう。

3. int はイミュータブル

まず、もっともシンプルな例として int を見てみましょう。

x = 10

前回の記事で説明したように、x という変数名は整数オブジェクト 10 を参照しています。ここで、x = 20 とすると、見た目には「x の値が10から20に変わった」ように感じます。

しかし、オブジェクトの視点から見ると、10 という整数オブジェクトそのものが 20 に変化したわけではありません。x が参照するオブジェクトが、10 から 20 に変わっています。

つまり、概念的には、

x ─→ 10

       ↓ 再代入

x ─→ 20

という変化です。

x = 20 は、整数オブジェクト 10 を変更する操作ではありません。x の参照先を別の整数オブジェクトに変える再代入です。

このように、int は作成されたオブジェクトそのものの内容を変更できません。そのため、イミュータブルな型です。

4. str もイミュータブル

文字列を表す str もイミュータブルです。たとえば、text = "Python" という文字列を考えてみましょう。

ここで、先頭の文字だけを変更しようとして、

text[0] = "J"

とすると、TypeError になります。str オブジェクトはイミュータブルなので、作成された文字列の一部だけを書き換えることはできないからです。

では、次のコードはなぜ実行できるのでしょうか。

text = "Python"
text = "Java"

これは、文字列オブジェクト "Python" を "Java" に変更したわけではありません。text の参照先が、"Python" という文字列オブジェクトから "Java" という別の文字列オブジェクトへ変わっています。

概念的には、次のような変化です。

text ─→ "Python"

           ↓ 再代入

text ─→ "Java"

イミュータブルとは、変数に別の値を再代入できないという意味ではありません。一度作られたオブジェクトそのものの内容を変更できない、という意味です。

ここは、ミュータブルとイミュータブルを理解するときに混同しやすいポイントです。

5. list はミュータブル

一方、list は代表的なミュータブルな型です。たとえば、numbers = [1, 2, 3] というリストを作ります。

ここで、numbers.append(4) を実行すると、リストは次のようになります。

[1, 2, 3, 4]

ここで起きていることは、int や str の再代入とは違います。numbers が別のリストオブジェクトを参照するようになったのではなく、numbers が参照している同じリストオブジェクトの内容が変更されています。

イミュータブルとミュータブルの違い。intでは参照先が変わり、listでは同じオブジェクトの内容が変わる

図の左側では、x = 20 によって x の参照先が整数オブジェクト 10 から 20 に変わっています。一方、右側では numbers の参照先は変わらず、同じリストオブジェクトに 4 が追加されています。

イミュータブルなオブジェクトへの再代入では「参照先」が変わり、ミュータブルなオブジェクトへの変更では「同じオブジェクトの内容」が変わります。

この違いが、Pythonでミュータブルとイミュータブルを理解するための中心になります。

5-1.同じリストを複数の変数名から参照するとどうなる?

ここで、前回の記事でも登場したコードをもう一度見てみましょう。

x = [1, 2, 3]
y = x

y.append(4)

print(x)
print(y)

実行結果は、

[1, 2, 3, 4]
[1, 2, 3, 4]

となります。y = x ではリストがコピーされるわけではなく、x と y が同じリストオブジェクトを参照します。

xとyが同じリストオブジェクトを参照し、yからの変更がxからも見える仕組み

図のように、x と y の参照先は同じです。そのため、y.append(4) でリストオブジェクト自体を変更すると、x から見ても変更後の状態になります。

ミュータブルなオブジェクトでは、複数の変数名が同じオブジェクトを参照していると、一方から加えた変更がもう一方からも見えます。

これは「ミュータブルだから自動的に複数の変数が連動する」という意味ではありません。同じオブジェクトを参照していることと、そのオブジェクトがミュータブルであることの2つが組み合わさることで、このような動きになります。

6. dict と set もミュータブル

list 以外にも、Pythonには代表的なミュータブルな型があります。その一つが dict です。

6-1.dict

次の辞書を考えてみましょう。

user = {
    "name": "Taro",
    "age": 20
}

ここで、user["age"] = 21 とすると、user が参照している同じ辞書オブジェクトの内容を変更できます。

{
    "name": "Taro",
    "age": 21
}

新しい辞書を作って user に再代入したわけではありません。同じ dict オブジェクトの状態が変更されています。

6-2.set

set もミュータブルです。

numbers = {1, 2, 3}
numbers.add(4)

print(numbers)

実行すると、集合に 4 が追加されます。

{1, 2, 3, 4}

この場合も、numbers が参照している同じ set オブジェクトの内容が変更されています。

list、dict、set はいずれも、同じオブジェクトのまま内容を追加・削除・変更できる代表的なミュータブル型です。

7. tuple はイミュータブル。でも「中身」には注意

ここまでの型の中で、少し注意したいのが tuple です。tuple はイミュータブルな型です。

たとえば、numbers = (1, 2, 3) というタプルに対して、numbers[0] = 10 のように要素を別のオブジェクトへ置き換えることはできません。

tuple 自体が保持している要素の参照を、あとから別のものへ置き換えることはできません。

ただし、ここには少しややこしいポイントがあります。次の tuple を考えてみましょう。

data = ([1, 2], [3, 4])

data が参照している tuple 自体はイミュータブルです。しかし、その要素として参照されているオブジェクトは list であり、list はミュータブルでした。

そのため、次のコードは実行できます。

data[0].append(5)

print(data)

結果は次のようになります。

([1, 2, 5], [3, 4])

「tuple はイミュータブルなのに、なぜ変更できたの?」と思うかもしれません。しかし、ここで変更されたのは tuple 自体ではなく、tuple の要素として参照されているリストオブジェクトです。

tuple自体はイミュータブルだが、tupleが参照するlistオブジェクトの内容は変更できる

図を見ると、tuple の要素が参照しているリストそのものは変わっていません。data[0].append(5) によって、その参照先であるリストオブジェクトの内容だけが変更されています。

tuple はイミュータブルなので、要素を別のオブジェクトへ置き換えることはできません。しかし、要素として参照しているオブジェクトがミュータブルなら、そのオブジェクト自体の内容は変更できます。

この例からも、「コンテナ」と「コンテナが参照しているオブジェクト」を分けて考えることが重要だと分かります。

8. ミュータブル・イミュータブルと「参照」の関係

ここまでの内容を、「変数とオブジェクト」という視点から整理してみましょう。

まず、int の例です。

x = 10
y = x

x = 20

最初は、x と y が同じ整数オブジェクト 10 を参照しています。しかし、x = 20 は 10 を変更する操作ではなく、x の参照先を別の整数オブジェクト 20 に変える操作です。

そのため、概念的には、

x ─→ 20

y ─→ 10

となります。

一方、list ではどうでしょうか。

x = [1, 2, 3]
y = x

x.append(4)

x と y は同じリストオブジェクトを参照しています。そして list はミュータブルなので、x.append(4) によって、そのリストオブジェクト自体を変更できます。

その結果、x と y のどちらから見ても [1, 2, 3, 4] となります。ここまでの int と list の違いを図で整理すると、次のようになります。

intの再代入とlistの変更を、変数名からオブジェクトへの参照で比較した図

int の例では、x = 20 によって x だけが別のオブジェクトを参照するようになります。一方、list の例では x と y の参照先は同じままで、そのオブジェクト自体が変更されています。

Pythonの動きを考えるときは、「どのオブジェクトを参照しているか」と「そのオブジェクトはミュータブルか、イミュータブルか」を分けて考えることが重要です。

この2つを組み合わせると、再代入やリストの変更といったPythonの動きを、一つの考え方で説明できるようになります。

9. なぜミュータブルとイミュータブルを知る必要がある?

ミュータブルとイミュータブルは、単にPythonの型を分類するための用語ではありません。

先ほど見たように、ミュータブルなオブジェクトを複数の変数名から参照していると、一方から加えた変更がもう一方からも見えます。この性質を知らないままコードを書いていると、「変更していないはずのデータまで変わった」という思わぬバグにつながることがあります。

また、この考え方は今後、

  • 代入とコピー
  • 浅いコピーと深いコピー
  • 関数の引数
  • 辞書やリストを使ったデータ処理
  • クラスのインスタンス

などを理解するときにも重要になります。

変数名がオブジェクトを参照する
↓
そのオブジェクトの型を考える
↓
ミュータブルなら同じオブジェクトを変更できる
↓
イミュータブルならオブジェクト自体は変更できない
↓
代入・コピー・関数の引数の理解につながる

ミュータブルとイミュータブルは、Pythonのさまざまな仕組みを理解するための土台の一つだと考えるとよいでしょう。

まとめ

Pythonのミュータブルとイミュータブルについて、今回のポイントをまとめます。

  • ミュータブルなオブジェクトは、同じオブジェクトのまま内容を変更できる
  • イミュータブルなオブジェクトは、作成されたオブジェクトの内容を変更できない
  • list、dict、set などはミュータブル
  • int、float、str、tuple などはイミュータブル
  • イミュータブルでも、変数名を別のオブジェクトへ再代入することはできる
  • tuple 自体はイミュータブルでも、要素として参照しているミュータブルなオブジェクトは変更できる
  • 同じミュータブルなオブジェクトを複数の変数名から参照すると、一方からの変更がもう一方からも見える

ミュータブル・イミュータブルを理解するときは、「変数の値が変わったか」ではなく、「同じオブジェクトそのものを変更できるか?」と考えることがポイントです。

そして、この考え方を前回の記事で説明した「Pythonの変数は箱ではなく、オブジェクトを参照する名前」という考え方と組み合わせると、Pythonの代入やデータ変更の仕組みがかなり理解しやすくなります。

次に理解したいのが、「代入」と「コピー」の違いです。ミュータブルなオブジェクトでは、単純に別の変数へ代入しただけではオブジェクトはコピーされません。

次の記事では、copy() と deepcopy() も含めて、Pythonのコピーの仕組みを詳しく見ていきます。

関連記事

前回の記事:Pythonの変数は「箱」ではない?|オブジェクトと参照を理解する

Pythonの変数は「箱」ではない?|オブジェクトと参照を理解する
Pythonの変数は本当に「値を入れる箱」なのでしょうか。オブジェクトと参照の仕組みを図解しながら、代入や再代入、ミュータブルとイミュータブル、コピー、参照カウントまでわかりやすく解説します。

次回の記事:Pythonの代入とコピーの違いとは?|copyとdeepcopyを理解する

Pythonの代入とコピーの違いとは?|copyとdeepcopyを理解する
Pythonの代入とコピーは何が違うのでしょうか。代入、浅いコピー、深いコピーの違いを、オブジェクトと参照の仕組みから図解。copyやdeepcopyの使い分けまで、具体例を使ってわかりやすく解説します。

関連書籍

Pythonの文法だけでなく、オブジェクトやデータモデルなど、Pythonの仕組みをもう少し深く理解したい人には『入門 Python 3』もおすすめです。

Amazon.co.jp

コメント

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