Python♪用語集:変更可能体(ミュータブル)と変更不能体(イミュータブル)

最初、Pythonの変更不能体(イミュータブル)という言葉を聞いて、私はJavaやCなどで用意されている「定数」をイメージしてしまったのですが、変更不能体(イミュータブル)は定数ではありません。 

他のプログラミング言語で用意されている「定数」は最初に変数を定数として宣言すると、その後、その変数に新しい数値を代入することができません。そのため変更したくない値は定数として宣言しておくと安心です。しかし、そもそも、Pythonには「定数」という考え方が存在しないのです。「定数」がないプログラミング言語は少数派(?)なのではないでしょうか。

Pythonにおいて数値型や文字列やタプルは変更不能体です。しかし、例えば変数aに「3」や「’abc’」や「(2, 3)」を代入した後に、それぞれa = 5とすれば、いずれも変数aは整数の「5」に入れ替わります。「変更できるのに変更不能体?」次々と疑問がわいてきます。

しかし、この疑問は代入した変数の参照先アドレス(id)に注目するとすっきりします。以下、参照先アドレス(id)に注目しながら解説したいと思います

0.Python♪ モヤモヤを解消する明快な用語集

「Python♪モヤモヤを解消する明快な用語集」の用語集Top(索引)はこちらです。

モヤモヤを解消する明快な用語集を目指します。例えば言葉の定義が「グレー」なものは「グレー」であると解説します。なお、同じ言葉でも、例えば「Python」と「Java」では定義が違うことがあります。 その場合、「python」での定義を解説します。

1.関連記事


参照先のアドレスの意味が分かりにくい方は、この記事を先にお読みください。

コンピューターの中で変数はどのように記憶されるのか。参照先のアドレスとは。 

以下の記事は、この記事と内容がほとんど重複していますが、「本当は変数には参照先のアドレス(id)しか入っていない」ことを説明するために、この記事に手を加えたものです。図が少しだけ難しくなっていますが、変更不能体のどこが変更できないのかはっきりします。読んでみてください。

Python♪本当は変数には参照先のアドレス(id)しか入っていない

2.変更可能体と変更不能体の分類

変更可能体(mutable、ミュータブル)と変更不能体(immutable、イミュータブル)の分類は以下の通りです。

(1) 変更可能体:リスト、セット、ディクショナリ型
(2) 変更不能体:数値型、文字列、タプル、ブール型、フローズンセット

3.数値型は変更不能体ってどういうこと?

コード01では変更可能体(ミュータブル)であるリストはa=[3]→[5]と変化します。

#コード1(リストの要素の変更)
a = [3]
a[0] = 5
print(a)
#出力1
[5]

一方、変更不能体(イミュータブル)である数値型もa=3→5と変化しました。それなのに、なぜリストは変更可能体で、数値型は変更不能体なのでしょうか。

#コード2(数値型の代入) 
a = 3
a = 5
print(a)
#出力2
5

4.部分的な変更が可能かどうかで区別

実は変更可能体(ミュータブル)とは、部分的に変更可能かどうかで分類します。リストは[2, 3]→[5, 3]のように部分的に変更が可能です。一方、変更不能体であるタプルは(2, 3)→(5, 3)のように部分的に変更することはできません。また、そもそも整数4は一部を変更するということが不可能です。

他のプログラミング言語の定数は、a = 3とすると、その後、全く変更を受け付けませんが、変更不能体は部分的に変えられないだけなのです。

ただ、そっくり入れ替えることはできるので、「変更不能」という言葉とのギャップから最初は戸惑ってしまいます。

5.参照先アドレス(id)

次に参照先アドレスに注目して、変更不能体を違った方向から眺めてみます。

例えば「a = 5」「a[0] = 5」「a = [5, 3]」のような変数aへの値の代入において、参照先のアドレス(id)がどのように変わるかに注目すれば、違いがはっきりします。

(1) 整数の代入の具体例

整数は変更不能体(イミュータブル)です。コード03では変数aを3から5に変更し、それぞれの参照先アドレス(id)を出力します。

Pythonでは id() と言う組み込み関数が用意されており、変数が参照しているメモリー上のアドレスを取得することができます。コンピューターのどこに記憶されているかを調べることができるのです。

4行目で変数aに5を代入する前と後では変数aの参照先アドレス(id)が変化しています。

#コード3
a = 3
print('代入前:', id(a))
a = 5
print('代入後:', id(a)) #数値を代入すると、idは変わってしまう。
#出力3
代入前: 1784770080
代入後: 1784770144

では、この状況を図で説明したいと思います。コード03の2行目では変数aに整数3を代入しました。出力3の2行目より、このとき整数3はコンピューターの中で「1784770080番地」に記憶されています。idはコンピューターの中の住所のようなものです。なお、図では 「1784770080番地」 を「i番地」としました。

次に、変数aに5を代入するとどうなるでしょうか。idは「1784770144番地」に変更されました。下の図のように、 a = 3で用意された整数3は放置され、全く新しいk番地に整数5が作られるのです。これは、変更ではなく、変数を新しく定義し直しています。つまり、再定義しているのです。

(2) 整数の代入とは、整数を再定義している

「変数の再定義」は、今の場所を消して上書きするのではなく、現在の記憶場所を放置し、別の記憶場所に定義し直します。

Pythonは、JavaやCのように型宣言を行いません。したがって、変数に値を代入したときに変数の型が決定されます。変数に整数を代入し直すということは、型宣言からやりなおしているのです。

(3) 変更不能体の代入は参照先アドレス(id)が必ず変わる

ここでのポイントは、既に変更不能体(イミュータブル)が代入されている変数を別の値に変更する場合や、変数に新たに変更不能体を代入する場合には、参照先アドレス(id)が必ず変わることです。

変更不能体は変更できないので再定義するしかないのです。

(4) タプルの代入の具体例

コード04、出力04を見てください。タプルも整数と同じく変更不能体(イミュータブル)なので同様の考え方ができます。

#コード04
a = (2, 3)
print('代入前:', id(a))
a = (5, 3)
print('代入前:', id(a)) #数値を代入すると、idは変わってしまう。
#出力04
代入前: 2685530772424
代入前: 2685530866952

コード03を図示すると以下のようになります。 タプルの内容を変更したいときには、下図のようにタプル全体を代入し直すことでしか内容を変更することはできません。もちろん、このとき参照先アドレス(id)は変わってしまいます。数値型の代入と同様にa = (2, 3)で用意された変数の箱は箱ごと放置され、新しいa = (5, 3)という箱に入れ替わります。変更ではなく、再定義されているのです。

タプルは変更不能体(イミュータブル)なので、コード05のように、タプルの要素の一部を変更しようとしてもエラーとなります。一部を変更することはできないのです。

#コード05
a = (2, 3)
a[0] = 5   #エラーになる

(5) リストの「要素の変更」の具体例

 一方、コード4はリストの「要素を」を変更した例です。リストの要素をa[0]=5と変更しても、参照先アドレス(id)は変わりません。a=[2, 3]で用意された箱はそのままで、0番目の要素の中身の2が5に変更されるのです。

#コード06
a = [2, 3]
print('代入前:', id(a))
a[0] = 5
print('代入後:', id(a)) #リストの要素を変更してもidは変わらない。
#出力06
代入前: 2685510936072
代入後: 2685510936072

(6) リスト全体を代入すると、再定義になる。

しかし、ここで注意しなければならないのは、変更可能体(ミュータブル)であっても変更可能体のすべてを入れ替えるような代入をした場合には、参照先アドレス(id)が変わってしまいます。

コード06ではa[0] = 5としましたが、コード07の4行目ではa = [5, 3]としました。出力07を見ると今度は参照先アドレス(id)が変わっているのが分かります。整数やタプルの時と同様に再定義されているのです。

再定義のときには左辺が「変数名 =」という形になっていることに注目しましょう。「変数名 = 値」のときは、右辺の値が変更可能体であったとしても再定義されるのです。

#コード07
a = [2, 3]
print('代入前:', id(a))
a = [5, 3]
print('代入後:', id(a)) #リストの要素を変更してもidは変わらない。
#出力07
代入前: 2685531776264
代入後: 2685531644040

6.「参照渡し」「浅いコピー」「深いコピー」の理解が深まる

この違いがわかれば、「参照渡し」「浅いコピー」「深いコピー」を理解する時に役に立つと思います。

コード08は、「参照渡し」の説明でよく使われる例ですが、b = aでは、参照先だけがaからbに渡されるので、aもbも同じアドレスの数値を参照先としています。つまり、aの「要素を」変更すると、bの要素の値も変わってしまいます。

#コード08
a = [3]
b = a #参照渡し
a[0] = 5
print('a, b', a, b) #参照渡しなので、aの要素を変更するとbの要素も変わる。
print('id(a), id(b)', id(a), id(b)) #a, bは同じid
#出力08
a, b [5] [5]
id(a), id(b) 2685531749192 2685531749192

一方、コード09ではリストの「要素を」変更したのではありません。4行目で再定義されているため参照先のアドレスも変わってしまいます。つまり、変数aはそれまで変数aに記憶していた[3]は放置し、別の場所にa=[5]に再定義するので変数bの値はかわりません。

#コード09
a = [3]
b = a   #参照渡し
a = [5]
print('a, b', a, b) #参照渡しでも、リストごと変えるとaだけ値が変わる。
print('id(a), id(b)', id(a), id(b)) #a = [5]は再定義なのでidが変わる。
#出力09
a, b [5] [3]
id(a), id(b) 2685531749704 2685531638984

 いかがでしょうか。参照先アドレス(id)の変化に着目しながら、変更可能体や変更不能体について考えることができれば、少し、違った目でPythonの変数を理解することができるようになります。

 ただし、NumPy配列については、上記のアドレス参照方法とは別の方法がとられていますので、注意が必要です。

私が実際に購入した教材のご紹介

以下、私が実際に購入したPythonの教材をまとめてみました。 Pythonを学習する上で、少しでもお役に立つことができればうれしいです。

Python♪私が購入したPythonの書籍のレビュー

UdemyのPythonの動画講座を書籍を買う感覚で購入してみた