Python♪変更可能体と変更不能体はidに注目。浅いコピーなどの理解に役立つ。

 Pythonの変更不能体という言葉を聞いて、私はjavaやVBAなど他のプログラミング言語で用意されている「定数」をイメージしてしまったのですが、Pythonの変更不能体は定数ではないのです。 

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

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

 変更可能体と変更不能体を理解するには変数の参照先アドレス(id)に注目するとすっきりします。そして、「参照渡し」「浅いコピー」「深いコピー」の理解にも役立ちます。
 以下、参照先アドレス(id)に注目しながら解説したいと思います

関連記事:
(1) idに触れない簡潔な説明は用語集「変更可能体と変更不能体」で行っています。
(2) コンピューターの中で変数はどのように記憶されるのか。参照先のアドレスとは。
 参照先のアドレスの意味が分かりにくい方は、この記事を先にお読みください。

変更可能体(mutable)と変更不能体(immutable)

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

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

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

 以下の例では、変更可能体のリストはa=[3]→[5]と変化し、変更不能体の数値型もa=3→5と変化しました。それなのに、なぜリストは変更可能体で、数値型は変更不能体なのでしょうか。

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

 

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

変更不能体とは、部分的な変更が可能かどうかで区別します。

 実は変更可能体とは、部分的に変更可能かどうかで分類します。リストは[1, 2]→[3, 2]のように部分的に変更が可能です。一方、変更不能体であるタプルは(1, 2)→(3, 2)というように部分的に変更することができません。また、数値4は、そもそも4の一部を変更するということが不可能です。
 他のプログラミング言語の定数は、a = 3とすると、その後、全く変更を受け付けませんが、Pythonの変更不能体は部分的に変えられないだけなのです。

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

 次に参照先アドレスに注目して、変更不能体について違った方向から眺めてみます。a = 5, a[0] = 5, a = [5]。これらの代入において、aの参照先のアドレス(id)がが変わるかどうかに注目すれば、それぞれの代入の違いがはっきりと分かります。
 変更可能体の「要素の」変更a[0] = 5は、変数aの参照先アドレス(id)は変わりません。一方、変更不能体の代入a = 5は、変数aの参照先アドレス(id)が変わります。
 さらに、変更可能体であっても、a = [5]のように内容を完全に入れ替えると、aの参照先アドレス(id)が変わってしまいます。以下、これが何を示しているのかを整理したいと思います。

数値の代入とは、数値を再定義している

 Pythonでは id() と言う組み込み関数が用意されており、変数が参照しているメモリー上のアドレスを取得することができます。
 コード3は数値型の変数を変更する例です。b=5を代入することで、変数aの参照先アドレス(id)が変わっています。つまり、b=3で用意された3が入った箱は放置され、全く新しいb=5という新しい箱が作られるのです。これは、変更ではなく、再定義しているのです。「変数の再定義」は、今の場所を消して上書きするのではなく、現在の記憶場所を放置し、別の記憶場所に定義し直します。
 Pythonは、javaやVBAのように型宣言を行いません。したがって、変数に値を代入したときに変数の型が決定されます。変数に数値を代入し直すということは、型宣言からやりなおしているのです。
 この様に考えれば、a=3 に対してa=5とすることは、aの値を変更しているわけではありません。a=3が変更できないので、a=3を初期化し、再定義しなおしているのです。

#コード3
b = 3
print('int_id_0 = ', id(b))
b = 5
print('int_id_1 = ', id(b)) #数値を代入すると、idは変わってしまう。
#出力3
int_id_0 =  1372089888
int_id_1 =  1372089952

リストの「要素の」変更とは

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

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

 

#出力4
代入前:  204820104
代入後:  204820104

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

 しかし、ここで注意しなければならないのは、変更可能体は「要素の」変更が可能なのです。つまり、変更可能体のすべてを入れ替えるような代入をした場合には、参照先アドレス(id)が変わってしまいます。以下の例ではコード4のa[0] = 5とは異なり、a = [5]としました。今度は参照先アドレス(id)が変わってしまいます。この場合は、変更可能体であってもa = [3]で用意した変数の箱は、箱ごと放置されa = [5]という新しい箱に入れ替わります。変更不能体の代入と同様に再定義されています。再定義のときには左辺が「変数名 =」という形になっていることに注目しましょう。

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

タプルが変更不能体だというのは理解しやすい

 

 タプルが変更不能体であるというのは、数値型より理解しやすいです。下図のように、タプルの要素の一部を変更しようとしてもエラーとなります。一部を変更することはできないのです。

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

「参照渡し」「浅いコピー」「深いコピー」の理解に役立つ

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

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

#コード6
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
#出力6
a, b [5] [5]
id(a), id(b) 204725448 204725448

 

 一方、コード7では、リストの「要素を」変更したのではありません。a=[5]とし、aにリスト[5]をまるまる代入しているため参照先のアドレスも変わってしまいます。変数aはそれまで変数に記憶していたことを白紙に戻し、a=[5]に再定義されるのでaのみ値がかわります。

#コード7
a = [3]
b = a #参照渡し
a = [5]
print('a, b',a, b) #参照渡しでも、リストごと変えるとaだけ値が変わる。
print('id(a), id(b)', id(a), id(b)) #aのidがa = [5]とすることで変わってしまう。
#出力7
a, b [5] [3]
id(a), id(b) 204818888 204725320

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

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