Pythonのリストのコピーでは多くの方が、予期せぬ挙動をしてつまづく経験をしています。そこでここでは、リストのコピーの方法と注意点を詳しく解説します。
1. リストのコピーの注意点
1.1. 「=」演算子を使う場合の不都合な点
リストのコピーというと、次のように「=」演算子を使う方法が真っ先に思い浮かぶかもしれません。
old_list = [1, 2, 3, 4, 5]
new_list = old_list # 元のリストを別の変数に代入
print(f'元リスト:{old_list}')
print(f'新リスト:{new_list}')
しかし、これでは元リストと新リストは同一オブジェクトということになります。具体的には、下図のように[1, 2, 3, 4, 5]というデータに、「new_list」と「old_list」という二つの呼び名(変数名)が付いている状態であるということです。なお、変数については「Pythonの変数の定義方法と命名規則」をご覧ください。
つまり、「old_list」と「new_list」は全く同じ場所に格納されているデータを共有しているということです。
そのため、変数「old_list」を操作して中のデータを変更するということは、変数「new_list」の中のデータを変更するのと同じことになります。
以下のコードをご覧ください。「old_list」のデータにappend()で、新しい要素6を追加しています。append()については「Pythonのappend()でリストに要素を追加」で解説しています。
new_list.append(6) # 新リストに要素を追加
print(f'元リスト:{old_list}')
print(f'新リスト:{new_list}')
変数「old_list」と変数「new_list」は全く同じデータを持っているので、「old_list」に加えた変更は、「new_list」にも反映されます。当然、「new_list」に変更を加えると「old_list」にも反映されます。次のコードをご覧ください。
old_list.append(7) # 元リストに要素を追加
print(f'元リスト:{old_list}')
print(f'新リスト:{new_list}')
このように一方の変数に変更を加えると、他方の変数にまで反映されてしまうと、それは必ず重大なエラーの原因となってしまいます。詳しくは「Pythonのオブジェクト指向プログラミングの知識と書き方」で解説しています。
1.2. リストはcopy関数でコピー
「=」演算子を使った方法は不都合なので、Pythonではリストのコピーは、copyモジュールのcopy関数を使って行います。この関数については「Pythonのcopy関数とdeepcopy関数」で解説しているので、ここではリストのコピーに焦点を当てて見ていきましょう。
次のようにcopy関数でリストをコピーできています。
import copy
old_list = [1, 2, 3, 4, 5]
new_list = copy.copy(old_list)
print(f'元リスト:{old_list}')
print(f'新リスト:{new_list}')
この方法の場合、全く同じ場所に格納されている同じデータに複数の変数名がついているのではなく、値が同じデータが別々の場所に格納されていて、そこに別々の変数名がつけられていることになります。
二つの変数は同じデータを共有しているわけではないため、一方のリストに変更を加えても他方のリストにそれが反映されることはありません。以下のコードで確認してみましょう。
new_list.append(6) # 新リストに要素を追加
print(f'元リスト:{old_list}')
print(f'新リスト:{new_list}')
old_list[2] = 0 # 元リストの要素を変更
print(f'元リスト:{old_list}')
print(f'新リスト:{new_list}')
それぞれ一方の変数の変更は他方には影響していませんね。
ただし、少しややこしいのですが多重リストになると必ずしもこの通りになるわけではありません。copy関数には、
- 浅いコピー
- 深いコピー
の2つがあって、多重リストの場合は、これが双方の変数への影響の仕方に違いを及ぼします。それを次から見ていきましょう。
2. 浅いコピー copy()
copy関数でコピーした場合、それは「浅いコピー」というものになります。浅いコピーの場合、一方の変数への要素の追加などは他方の変数には影響しません。しかし、双方が共通して持っているデータを変更すると、双方とも影響します。
実際に見ていきましょう。以下のコードは「old_list」とそれを浅いコピーで作った「new_list」です。
import copy
old_list = [[1, 1], [2, 2], [3, 3]]
new_list = copy.copy(old_list) # 浅いコピー
print(f'元リスト:{old_list}')
print(f'新リスト:{new_list}')
多重リストの浅いコピーでは、リストの要素の追加や削除の場合は、他方の変更は一方には影響しません。
a = [4, 4]
new_list.append(a) # 新リストに要素を追加
print(f'元リスト:{old_list}')
print(f'新リスト:{new_list}')
new_list.pop() # 新リストの要素を削除
print(f'元リスト:{old_list}')
print(f'新リスト:{new_list}')
しかし、両者が共通して持っている値の変更の場合は、一方に加えた変更は他方にも影響します。
import copy
old_list = [[1, 1], [2, 2], [3, 3]]
new_list = copy.copy(old_list)
old_list[1][0] = 'A' # 元リストの要素の一部を変更
print(f'元リスト:{old_list}')
print(f'新リスト:{new_list}')
new_list[2][1] = 'C' # 新リストの要素の一部を変更
print(f'元リスト:{old_list}')
print(f'新リスト:{new_list}')
それぞれ一方にしか変更を加えていないのに、その変更は他方にも反映されていますね。
3. 深いコピー deepcopy()
それでは、相互に全く影響を及ぼし合わない方法でコピーするにはどうすればいいのでしょうか。その場合はdeepcopy関数を使います。
「copy.copy()」を「copy.deepcopy()」にするだけです。これでリストをコピーすると、意図せぬデータ変更を気にする必要なく、元リストも新リストも自由に操作することができます。
import copy
old_list = [[1, 1], [2, 2], [3, 3]]
new_list = copy.deepcopy(old_list) # 深いコピー
print(f'元リスト:{old_list}')
print(f'新リスト:{new_list}')
確認しておきましょう。他方への要素の追加や削除、共通して持っている値の変更のどれも、お互いに影響を及ぼしあってはいませんね。
a = [4, 4]
new_list.append(a) # 新リストに要素を追加
print(f'元リスト:{old_list}')
print(f'新リスト:{new_list}')
import copy
old_list = [[1, 1], [2, 2], [3, 3]]
new_list = copy.deepcopy(old_list)
old_list[1][0] = 'A' # 元リストの要素の一部を変更
print(f'元リスト:{old_list}')
print(f'新リスト:{new_list}')
new_list[2][1] = 'C' # 新リストに要素を追加
print(f'元リスト:{old_list}')
print(f'新リスト:{new_list}')
このためリストをコピーする時は、特殊な処理をする意図がないなら「深いコピー」をするようにすると良いでしょう。
4. まとめ
まずPythonのリストをコピーする時は、安易に「=」演算子を使わないように意識しましょう。「浅いコピー」と「深いコピー」があることを理解して、目的に応じて使い分けられるようになることが肝心です。
なお、一重リストの場合は、copy.copy()を使ったコピーでも予期せぬ値変更の問題は私が知る限りはありません。ただし多重リストになるとcopy.copy()では予期せぬ変更が起きます。その場合はcopy.deepcopy()の選択肢があることを思い出せるようにしておきましょう。
コメント