LoginSignup
68
61

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-11-13

はじめに

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

やりたかったこと

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

68
61
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
68
61