#はじめに
リストを他の変数にコピーしようと思ったときに思うように行かず、無駄な時間を過ごしたので自戒の意味を込めて記事を書きます。
#やりたかったこと
a = [0, 1, 2, 3, 4]
b = []
という変数があり、
- bにaを追加して
- aを変更
- 1〜2を繰り返し
ということをしようとしました。
そのときのコードを簡略化したのが以下のもの。
print(b) # 変数に何も入っていないことの確認
b.append(a) # bにaを追加(1回目)
print(b)
a[2] = 99 # aの[2]番目の要素を書き換え
b.append(a) # bにaを追加(2回目)
print(b)
これを実行すると以下のように表示されます。
[]
[[0, 1, 2, 3, 4]]
[[0, 1, 99, 3, 4], [0, 1, 99, 3, 4]]
結果の3行目に注目すると、1回目に代入した[0, 1, 2, 3, 4]
というリストの[2]番目の要素がa[2] = 99
の代入をしたことによって変化してしまいました。
この動作について調べると、IDの受け渡しが行われていることに起因するもののようです。
#ID
Pythonのオブジェクトには、固有の識別値が割り振られています。
例えば、
print(id(a))
によって識別値を表示したとすると、私のPC上では、結果は以下のようになりました。
4392823368
#appendの動作
appendを使ってリストに値を追加したとき、このIDがそのまま受け渡されています。
(後述しますが、代入の場合もIDが受け渡されています)
a = [10]
print(id(a)) # ID: 4315448960
b = []
b.append(a)
print(id(b[0])) # ID: 4315448960
ということで、appendによって追加されたb[0]はaを参照しているだけなのです。[^1]
appendをもう一度やってみると、
a = [10]
id(a) # ID: 4315448960
b = []
b.append(a)
print(id(b[0])) # ID: 4315448960
b.append(a)
print(id(b[1])) # ID: 4315448960
と、b[0]とb[1]のIDは同じものになります。
そしてbの中身は[[10], [10]]
という状態
この状態でa[0] = 100
と代入してみると、bの中身が[[100], [100]]
に変化しています。
つまり、appendによって、aのIDのみが受け渡されているために、bの要素はaを参照しているに過ぎません。
なので、a自体を書き換えてしまうとbも変わってしまうという罠があるのです。
##ちなみに
代入するときも同じことが起きます。
例えば、
a = [10]
b = a
a[0] = 1234
print(b)
とすると、bの値は[1234]
と出てきます。
代入のときも同じようにIDの受け渡しが行われているからです。
これが普通の変数の場合だともちろん起きません。
a = 10
b = a
a = 1234
print(b) # 10と表示される
これは、通常の変数もIDを受け渡していますが、代入が行われるときに、IDの変更も同時に行われるためです。
(というか数字自体がIDを持っているっぽく、その数字のIDが代入されるらしい)
print(id(1)) # ID: 4308878688
print(id(2)) # ID: 4308878720
a = 1
print(id(a)) # ID: 4308878688
b = a
print(id(b)) # ID: 4308878688
a = 2
print(id(a)) # ID: 4308878720
print(id(b)) # ID: 4308878688
##リストの場合
代入でもappendでもIDの受け渡しによってそれらが実現されていたことがわかりました。
しかし、リストになるとおかしなことが起こるのは、リストの代入やappendのときにはリスト自体のidが受け渡されてしまうからなのです。
print(id(1)) # ID: 4308878688
a = [1]
print(id(a)) # ID: 4313304392(リスト自体のID)
print(id(a[0])) # ID: 4308878688
b = a
print(id(b)) # ID: 4313304392
リスト自体のIDが受け渡されることによって、bはaを参照しています。
ここでaのリストの値を書き換えることで、aとともにbも変わってしまいます。
このようなことは、リスト型以外でもディクショナリ型、セット型で起こり得ます。(これらをミュータブルな型と言います)
逆に起きないのは文字列、数値、タプルなどのイミュータブル(不変という意味)な型の場合です。
#対策
代入やappendをする場合に、IDがそのまま受け渡されることが原因でした。
これはcopyモジュールのdeepcopy関数を使うことで回避することができます。
import copy
a = [1]
b = []
b.append(copy.deepcopy(a)) # IDを変えたリストを返す
a[0] = 2
とすると、bの中身は[2]
とならずに、[1]
のままです。
deepcopy関数は、aの中身はそのままに、IDを変えたリストを返します。
これによって、リストをappendしていくことができるようになります。
代入するときも同様にb = copy.deepcopy(a)
とすることでaへの代入の影響を受けなくなります。
#追記
@toritoritorinaさんからコピーの方法について教えていただいたので、追加です。ありがとうございます。
deepcopy以外にもまだまだ方法があったとは!
#他のリストコピーの方法
import copy a = [1] b = a[:] # [:]によるコピー b = a.copy() # リストのコピーメソッド b = copy.copy(a) # copyモジュールの浅いコピー b = copy.deepcopy(a) # copyモジュールの深いコピー
#深いコピー
import copy a = [[1]] b1 = a[:] # [:]によるコピー b2 = a.copy() # リストのコピーメソッド b3 = copy.copy(a) # copyモジュールの浅いコピー b4 = copy.deepcopy(a) # copyモジュールの深いコピー a[0][0] = 2
リスト内にリストがあるような場合少し注意が必要です。
a[0][0] = 2
とすると、b1, b2, b3は影響を受けます。これは中身のリスト..a[0]
が同一のリストを指しているためです。
この場合は、b4のようにdeepcopy()を使うのがベターです。
#おわりに
ミュータブルな型の場合の代入やappendにはくれぐれも気をつけましょう。
※間違いがあった場合はご指摘ください
#参考
Python のリストの扱いで注意すること
[Pythonの変数とオブジェクトについて]
(https://qiita.com/makotoo2/items/35f8c2abf3248816f0e4)