Pythonはcopy関数を使って、オブジェクトのコピーを作ることができます。ただし、コピーにはshallowcopy(浅いコピー)とdeepcopy(深いコピー)という概念があります。この違いを理解していなかったことでドツボにハマる方もいますので、それぞれの違いと使い方に関して詳しく解説していきます。
【重要】オブジェクトIDについて
copy関数についての解説の前に、まずオブジェクトIDについて解説しておきます。これは、特に初級者、中級者のうちはドツボにハマりやすいものなので、しっかりお読みください。
Pythonでは、=演算子を使ってオブジェクトのコピーを作ることができます。しかし、実はこの方法では別の新しいオブジェクトを作っているのではなく、同じデータを参照する別の変数を作っていることになります。
次のコードをご覧ください。
''' =演算子でコピーした場合 '''
old_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
new_list = old_list
print(f'元のリスト:{old_list}')
print('元のリストのID番号:', id(old_list))
print(f'新しいリスト:{new_list}')
print('元のリストのID番号:', id(new_list))
ここではまず変数old_listのデータを=演算子でコピーした新しい変数new_listを作っています。注目して欲しいのが、両変数のID番号です。どちらも全く同じ番号ですね。これは、2つの変数は別のオブジェクトではなく、全く同一のオブジェクトであるということを意味します。
同一オブジェクトであるということは、どちらか一方の変数に加えた変更は、もう一方の変数にも影響するということを意味します。次のコードをご覧ください。
''' オブジェクトのID番号が同じなので一方に行った全ての変更は他方にも反映される。 '''
# 元のリストに新しい要素を追加
old_list.append ([10, 11, 12])
# 新しいリストにも変更が反映される。
print(f'元のリスト:{old_list}')
print(f'新しいリスト:{new_list}')
# 新しいリストから要素を削除
new_list.pop()
# 元のリストにも変更が反映される。
print(f'元のリスト:{old_list}')
print(f'新しいリスト:{new_list}')
まず、old_listにappendメソッドを使って新たな要素を追加しています。この時、別の変数new_listのデータも同じように変化していることが確認できますね。次に、new_listにpopメソッドを使って要素を削除しました。この時、もう一方の変数old_listのデータも同じように変化していますね。
このように、old_listと、old_listを=演算子でコピーした変数new_listは全く同じオブジェクトなのです。そのため、一方への変更は、そのままもう他方にも影響します。
プログラミングでは、こうした変数の扱いを理解していなければ、大きなプログラムを書く時に非常に苦労することになります。こうした変数の扱いによる混乱を避けるために、オブジェクト指向プログラミングというプログラミングパラダイムが生まれました。
プログラマーとして活躍するには、オブジェクト指向プログラミングはいまや必要不可欠なので、詳しくは『Pythonのオブジェクト指向プログラミングの知識と書き方まとめ』で確認しましょう。
さて、それでは以上の点を踏まえて上でcopy関数の浅いコピーと深いコピーというものを見ていきましょう。
1. copy関数の使い方(浅いコピーと深いコピー)
まずcopy関数の使い方は次の通りです。
''' 浅いコピーと深いコピー '''
import copy # copy関数を使うにはcopyモジュールをインポートする必要があります。
copy.copy(オブジェクト) # 浅いコピー
copy.deepcopy(オブジェク) # 深いコピー
浅いコピーを行うにはcopy()を、深いコピーを行うにはdeepcopy()を使います。それでは、浅いコピー、深いコピーとは何なのでしょうか。具体的に見ていきましょう。
1.1. Pythonのcopy関数(浅いコピー)
以下のコードはcopy()を使って、old_listの浅いコピーを作ったものです。
''' copy関数は浅いコピー '''
import copy
old_list = [[1, 2, 3], [4, 5, 6], [ 7, 8, 9]]
new_list = copy.copy(old_list)
print(f'元のリスト:{old_list}')
print('元のリストのID番号:', id(new_list))
print(f'新しいリスト:{new_list}')
print('古いリストのID番号:', id(old_list))
ここで、それぞれのオブジェクトのID番号をご覧ください。それぞれ異なるID番号になっているのでこれらは同一オブジェクトではありません。次のコードでは、appendメソッドを使って、old_listに新しい要素を追加しています。どのようになるか確認してみましょう。
なおappendメソッドについては『Pythonのappendの使い方とよく聞かれることまとめ』をご覧ください。
''' old_listの更新がnew_listに影響しない場合 '''
import copy
old_list = [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
new_list = copy.copy(old_list)
old_list.append([4, 4, 4])
print(f'元のリスト:{old_list}')
print(f'新しいリスト:{new_list}')
ご覧のように、元のリストold_listに追加した新しい要素は、新しリストnew_listには影響していませんね。しかし、ちょっと待ってください。次に以下のコードをご覧ください。
今度は、old_listとnew_listが共通に持っている要素に変更を加えました。
''' old_listの更新がnew_listに影響する場合 '''
import copy
old_list = [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
new_list = copy.copy(old_list)
old_list[1][1] = 'aa'
print(f'元のリスト:{old_list}')
print(f'新しいリスト:{new_list}')
今度は、元のリストold_listに加えた変更が、新しいリストnew_listにも反映されています。浅いコピーでは、コピー元のオブジェクトとコピー先のオブジェクトが共通して持っている要素に変更を加えた時は、その変更は双方に影響を及ぼします。
先ほど、old_listにappendメソッドで新しい要素を追加した時は、それは、コピー元のold_listとコピー先のnew_listが共通して持っている要素の変更ではありませんでしたね。その場合は、一方への変更は他方には反映されません。
しかし、今回は、お互いが共通して持っている要素の変更だったため、一方の変更が他方に影響したのです。これが浅いコピーです。もちろん、両方が共通して持っている要素の変更の場合、new_listに加えた変更もold_listに波及します。
''' 共通して持っている要素の変更はnew_listの更新でもold_listに波及する '''
import copy
old_list = [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
new_list = copy.copy(old_list)
new_list[1][0] = 'BB' #new_listに変更を加える
print(f'元のリスト:{old_list}')
print(f'新しいリスト:{new_list}')
以上がPythonのcopy()、「浅いコピー」です。
1.2. Pythonのdeepcopy関数(深いコピー)
続いて、Pythonのdeepcopy関数、「深いコピー」を解説します。結論から言うと深いコピーでは、一方のいかなる変更も他方には全く影響を与えない完全に別物のオブジェクトを作ることができます。
まず、次のコードでold_listを深いコピーをしてnew_listを作りました。
''' deepcopy関数は深いコピー '''
import copy
old_list = [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
new_list = copy.deepcopy(old_list) # deepcopy関数でコピーします。
print(f'元のリスト:{old_list}')
print('元のリストのID番号:', id(new_list))
print(f'新しいリスト:{new_list}')
print('古いリストのID番号:', id(old_list))
それでは、old_listとnew_listが共通してもつ要素に変更を加えてみましょう。
''' 深いコピーは一方がのように更新されても他方は影響を受けない '''
import copy
old_list = [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
new_list = copy.deepcopy(old_list)
old_list[1][0] = 'BB'
print(f'元のリスト:{old_list}')
print(f'新しいリスト:{new_list}')
いかがでしょうか。「朝いコピー」では一方の共通要素の変更は他方にも影響していましたが、「深いコピー」では全く影響がありませんね。これが「深いコピー」です。
1.3. =によるコピー、浅いコピー、深いコピーのまとめ
=演算子によるコピーは、厳密にはコピーではなく同じデータをもつ複数の変数を作るというものです。これは同じデータに対して呼び名を複数つけたのと同じことです。つまり、100というデータに、Aという呼び名とBという呼び名をつけたということです。この100というデータを90に変えた時は、Aと呼んでもBと呼んでもデータは90です。中身のデータは完全に同じオブジェクトのため、Aから変更を加えてもBから変更を加えても、その変更は両変数に影響します。
copy関数による浅いコピーは、オブジェクトのID番号が別になります。しかし、コピー元オブジェクトold_copyとコピー先オブジェクトnew_copyは完全に別物ではなく、あくまでも部分的に別物です。「部分的に別物」とは具体的には、両オブジェクトが共通して持っている要素は同一ということです。そのため=演算子のように他方の変更の全てが一方に反映されるわけではありませんが、お互いが共通して持つ要素への変更は、双方に影響します。
最後にdeepcopy関数による深いコピーは、完全に別物のオブジェクトを作ります。深いコピーでは、一方へのいかなる変更も他方には影響しません。
これらの違いを必ずしっかりと抑えておいて下さい。
2. Pythonのcopy関数は様々な型のオブジェクトに使える
ここまででPythonのcopyに関して重要なことはお伝えしました。浅いコピーと深いコピーは、これを知らなかったことが原因でドツボにハマる方はとても多いので、必ず理解しておきましょう。
ここからはある意味蛇足ですが、copy関数が様々な型のオブジェクトに使えるということを、実際のコードで掲載しておきます。
2.1. Pythonのlistのcopy
もちろん、Pythonのlistはcopy関数でコピーすることができます。なおリストだけでなくタプルも同様です。ここではリストのみ書いておきます。
''' listのcopy(tupleも同様) '''
import copy
list = [1, 2, 3, 4, 5]
s_list = copy.copy(list)
d_list = copy.deepcopy(list)
print(list) # 元データ
print(s_list) # 浅いコピー
print(d_list) # 深いコピー
そのままコピーできていますね。
2.2. Pythonのdictのcopy
続いて、Pythonのdict(辞書)もコピーすることができます。以下、ご覧ください。
''' dictのcopy '''
import copy
dict = {'a': 1, 'b':2, 'c':3}
s_dict = copy.copy(dict)
d_dict = copy.deepcopy(dict)
print(dict) # 元データ
print(s_dict) # 浅いコピー
print(d_dict) # 深いコピー
2.3. Pythonのarrayのcopy
PythonのNumpyのarrayのcopyも、同じcopy関数で行うことができます。
''' numpyのarrayのcopy '''
import numpy as np
import copy
array = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
s_array = copy.copy(array)
d_array = copy.deepcopy(array)
print(array) # 元データ
print(s_array) # 浅いコピー
print(d_array) # 深いコピー
2.4. Pythonのsetのcopy
Pythonのsetも同様ですね。
''' setのcopy '''
import copy
set = {100, 200, 300}
s_set = copy.copy(set)
d_set = copy.deepcopy(set)
print(set) # 元データ
print(s_set) # 浅いコピー
print(d_set) # 深いコピー
2.5. 【注】Pythonのfileのcopyはshutil.copy関数を使う
これは知っている人にとっては当たり前ですが、初心者は間違えるかもしれないので解説しておきます。Pythonではファイルのコピーは、copy.copy()ではなく、shutilモジュールのshutil.copy関数を使います。
次のように書きます。
import shutil
shutil.copy('コピーしたいファイル,' 'コピー先ディレクトリ')
こちらは別途、解説記事を用意したいと思います。
3. まとめ
Pythonでのオブジェクトのコピーには、通常、
- =演算子
- copy.copy関数(浅いコピー)
- copy.deepcopy関数(深いコピー)
があります。
=演算子を使ったものは、厳密にはコピーというより、同じデータに別の名前をつけるという行為です。この場合、元の変数に対して行った全ての変更が新しい変数にも反映されます。
copy.copy関数を使った浅いコピーは、部分的に別のオブジェクトを作りますが、コピー元とコピー先が共通して持つ要素に対する変更は、双方に反映されます。
copy.deepcopy関数を使った深いコピーは完全に別物のオブジェクトを作ります。一方に行ったいかなる変更も他方には反映されません。
なお、copy関数、deepcopy関数は様々なオブジェクトに対して使えますが、ファイルのコピーは、shutilモジュールのshutil.copy関数を使います。この点だけ誤解のないようにしましょう。
コメント