Edited at

リストのコピーでハマった話

More than 1 year has passed since last update.


はじめに

リストを他の変数にコピーしようと思ったときに思うように行かず、無駄な時間を過ごしたので自戒の意味を込めて記事を書きます。


やりたかったこと

a = [0, 1, 2, 3, 4]

b = []

という変数があり、


  1. bにaを追加して

  2. aを変更

  3. 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の変数とオブジェクトについて