python3
リストの代入
深いコピー

pythonでのリストの要素の変更とリストのコピー

pythonでリストを扱っているいると不可解な現象が??

 list1の要素を変更させないために、別の変数(list2)へlist1を代入し、そのlist2の要素(list2[0]=0,list2[1]=0)を変更します。すると、不可解なことにlist1の要素まで変更してしまうのです。

>>> list1=[1,2,3,4,5]
>>> list2=list1
>>> list1
[1, 2, 3, 4, 5]
>>> list2
[1, 2, 3, 4, 5]
>>> list2[0]=0
>>> list2[1]=0
>>> list2
[0, 0, 3, 4, 5]
>>> list1
[0, 0, 3, 4, 5]
>>> 

 このコードは[1,2,3,4,5]が入った箱1を作成し、箱1の中身が変わらないように箱1を別の箱2にコピーし、箱2の中身を変えるというイメージで記述したものですが、イメージ通りに動作していません。他に記述のしようが無いくらい単純なコードなので「pythonに欠陥がある」とか「動作に不具合がある」と思ってしまうかも知れませんが、動作が正しいことを前提としてイメージのどこに欠陥があるのか考えてみてください。
 実は、list2の変更内容とlist1が同じになっていることからわかるように、このコードで箱(list1)はコピーされていません。このことは次のようにidという関数を利用して直接見ることができます。

>>> id(list1)
643765640
>>> id(list2)
643765640

 "id"という関数を用いて返された値がlist1及びlist2ともに"643765640"という数字であることがわかります。この数字はリストに付されたIDを示しています。つまり、このIDを参照(リファレンス)すれば、list1とlist2があることを表しています。最初に記述したコードは正しいイメージで表現するなら、箱をコピーしたのではなく、箱の格納場所が記載されたメモをコピーしただけということになります。
 さて、少し脇道にそれますが、次にリストの要素のIDを調べてみると、また不可解なことが起こります。

>>> id(list1[0])
1750847184
>>> id(list2[0])
1750847184
>>> id(list1[1])
1750847184
>>> id(list2[1])
1750847184
>>> id(list1[2])
1750847280
>>> id(list2[2])
1750847280
>>> 

 list1[0],list2[0],list1[1]及びlist2[2]のIDがすべて同一の"1750847184"という数字になっています。これをさっきのロジックで考えると、例えばlist1[0]=2を代入するとlist1[1],list2[0]及びlist2[1]がすべて2に変わってしまいそうです。
 しかし、そのようなことは起こりませんのでご安心ください。リストの内容は[0, 0, 3, 4, 5]ですので、list1[0],list2[0],list1[1]及びlist2[2]は同じ数字"0"のIDを示しているだけです。そして、list1[0]=2を実行するとlist1[0]のIDが数字"2"を示すID"1750847248"に変化します。なお、数字そのものに代入しようと0=4などを実行してみても当然のことながら、エラーになります。

>>> a=0
>>> id(a)
1750847184
>>> id(0)
1750847184
>>> list1[0]=2
>>> id(list1[0])
1750847248
>>> list1
[2, 0, 3, 4, 5]
>>>

リストを箱ごとコピーするにはどうすれば良いか?

 少々脱線しましたが、リストを箱ごとコピーするにはcopyというモジュールにあるcopy()もしくはdeepcopy()という関数を利用します。copy()によるコピーを浅いコピーdeepcopy()によるコピーを深いコピーを呼んでいますが、簡単に言うと単純なリストなら浅いコピーで問題ありませんが、リストの中にさらにリストがあるような場合は深いコピーを使う必要があります。浅いコピーの場合は、コピーによりできたリストには新たなIDが付与されますが、リストの要素のIDはコピー元と同じです。従って、リストの要素が数値や文字列ならそこに代入することができないため、リストの要素を変更しても互いに影響しませんが(変更すると新たなIDが付与される)、リストの要素がリスト(入れ子リスト)である場合には入れ子リストのIDがコピーされるだけであるため、リストの代入と同様な現象が起こります。

浅いコピー(copy.copy())

>>> import copy
>>> list1=[1,2,3,4,5]
>>> list2=copy.copy(list1)
>>> id(list1)
643765192
>>> id(list2)
643757512
>>> list2[1]=0
>>> list2
[1, 0, 3, 4, 5]
>>> list1
[1, 2, 3, 4, 5]
>>> list1=[1,[1,1],3,4,5]
>>> list2=copy.copy(list1)
>>> list2[1][1]=1000
>>> list2
[1, [1, 1000], 3, 4, 5]
>>> list1
[1, [1, 1000], 3, 4, 5]
>>> id(list1[1])
643765640
>>> id(list2[1])
643765640

深いコピー(copy.deepcopy())

import copy
>>> list1=[1,[1,1],3,4,5]
>>> list2=copy.deepcopy(list1)
>>> list2[1][1]=1000
>>> list1
[1, [1, 1], 3, 4, 5]
>>> list2
[1, [1, 1000], 3, 4, 5]
>>> id(list1[1])
643756744
id(list2[1])
473497288

当然のことですが、単なるリストではなくてリストの入れ子構造である多次元の配列をコピーする際には、必ず深いコピー(deepcopy())を利用する必要があります。

>>> matrix=[[1,0,0],[0,1,0],[0,0,1]]
>>> copymatrix=copy.copy(matrix)
>>> copymatrix[0][1]=10
>>> copymatrix
[[1, 10, 0], [0, 1, 0], [0, 0, 1]]
>>> matrix
[[1, 10, 0], [0, 1, 0], [0, 0, 1]]
>>> matrix=[[1,0,0],[0,1,0],[0,0,1]]
>>> copymatrix=copy.deepcopy(matrix)
>>> copymatrix[0][1]=10
>>> copymatrix
[[1, 10, 0], [0, 1, 0], [0, 0, 1]]
>>> matrix
[[1, 0, 0], [0, 1, 0], [0, 0, 1]]

この問題には、深いコピーを知らずに行列を扱っていて、非常に悩まされたました。