[解決!Python]深いコピーを作成するには:解決!Python
浅いコピーと深いコピーの違い、copyモジュールが提供するdeepcopy関数の使い方、深いコピーを作成する関数の実装例を紹介する。
list_0 = [0, 1, 2]
list_1 = [3, 4, 5]
list_2 = [list_0, list_1]
print(list_2) # [[0, 1, 2], [3, 4, 5]]
list_3 = list_2.copy() # 浅いコピー
print(list_3) # [[0, 1, 2], [3, 4, 5]]
list_3[0][0] = 100 # 浅いコピーにより、list_2の要素も変更されてしまう
print(list_3) # [[100, 1, 2], [3, 4, 5]]
print(list_2) # [[100, 1, 2], [3, 4, 5]]
from copy import deepcopy
list_4 = deepcopy(list_2) # 深いコピー
print(list_2) # [[100, 1, 2], [3, 4, 5]]
print(list_4) # [[100, 1, 2], [3, 4, 5]]
list_4[0][0] = 0 # 深いコピーなら、list_4の要素を変更しても、list_2の要素は変更されない
print(list_4) # [[0, 1, 2], [3, 4, 5]]
print(list_2) # [[100, 1, 2], [3, 4, 5]]
from collections.abc import Mapping, Sequence
# 循環参照を考慮しない深いコピー
def mydeepcopy(obj):
try: # ハッシュ可能(イミュータブル)なオブジェクトはそのまま返す
hash(obj)
return obj
except TypeError:
pass
obj_type = obj.__class__ # オブジェクトの型を取得
if isinstance(obj, Sequence): # シーケンスは各要素を再帰的にコピー
return obj_type(mydeepcopy(e) for e in obj)
if isinstance(obj, Mapping): # キーはそのまま、値を再帰的にコピー
tmp = {k: mydeepcopy(v) for k, v in obj.items()}
return obj_type(tmp)
return obj
list_0 = [0, 1, 2]
list_1 = [3, 4, 5]
list_2 = [list_0, list_1]
list_3 = mydeepcopy(list_2)
print(list_2) # [[0, 1, 2], [3, 4, 5]]
print(list_3) # [[0, 1, 2], [3, 4, 5]]
list_3[0][0] = 100
print(list_2) # [[0, 1, 2], [3, 4, 5]]
print(list_3) # [[100, 1, 2], [3, 4, 5]]
list_4 = [0]
list_4.append(list_4)
print(list_4) # [0, [...]]
list_5 = mydeepcopy(list_4) # RecursionError
# 循環参照を考慮する深いコピー
import array
def mydeepcopy(obj, memo=None):
if memo is None:
memo = {}
if id(obj) in memo:
return memo[id(obj)]
try: # ハッシュ可能(イミュータブル)なオブジェクトはそのまま返す
hash(obj)
return obj
except TypeError:
pass
obj_type = obj.__class__ # オブジェクトの型を取得
if isinstance(obj, tuple): # ミュータブルな要素を含むタプル
copied = obj_type(mydeepcopy(item, memo) for item in obj)
memo[id(obj)] = copied
return copied
if isinstance(obj, Sequence): # それ以外のシーケンス
if isinstance(obj, array.array):
copied = obj_type(obj.typecode)
else:
copied = obj_type()
memo[id(obj)] = copied
copied.extend(mydeepcopy(item, memo) for item in obj)
return copied
if isinstance(obj, Mapping): # キーはそのまま、値を再帰的にコピー
copied = obj_type()
memo[id(obj)] = copied
copied.update({k: mydeepcopy(v, memo) for k, v in obj.items()})
return copied
# その他のオブジェクト(集合など)はそのまま返す
memo[id(obj)] = obj
return obj
list_4 = [0]
list_4.append(list_4)
print(list_4) # [0, [...]]
list_5 = mydeepcopy(list_4) # OK
print(list_5) # [0, [...]]
list_5[0] = 1
print(list_4) # [0, [...]]
print(list_5) # [1, [...]]
list_4[1][0] = 100
print(list_4) # [100, [...]]
print(list_5) # [1, [...]]
浅いコピーと深いコピー
Pythonのリストをcopyメソッドで複製すると、それはリストの浅いコピーになる。浅いコピーとは、コピー元のオブジェクトが含む要素への参照を含むコピーのことだ。これに対して、深いコピーがある。これはコピー元のオブジェクトが含む要素も全て複製するコピーのことである。
コードにすると分かりやすい。
list_0 = [0, 1, 2]
list_1 = [3, 4, 5]
list_2 = [list_0, list_1]
print(list_2) # [[0, 1, 2], [3, 4, 5]]
ここではリストlist_2は2つのリストを要素として持つ。これをcopyメソッドで複製すると、リストlist_2の浅いコピーが作成される。
list_3 = list_2.copy() # 浅いコピー
print(list_3) # [[0, 1, 2], [3, 4, 5]]
ここで浅いコピーはコピー元のオブジェクト(リストlist_2)の要素への参照を含んでいる。
そのため、以下のようにリストlist_3の要素であるリストの要素を変更すると、コピー元のリストであるlist_2の要素も変更されてしまう。
list_3[0][0] = 100 # 浅いコピーにより、list_2の要素も変更されてしまう
print(list_3) # [[100, 1, 2], [3, 4, 5]]
print(list_2) # [[100, 1, 2], [3, 4, 5]]
これはリストlist_3の第0要素(list_0)の第0要素(0)を100に変更することで、コピー元のリストであるlist_2にもその変更の影響が及んだ例だ。こうした挙動が適切であれば問題はない(複数のオブジェクトで何らかの情報を共有したい場合など)。そうではなく、コピー元のオブジェクトの全ての複製が必要であれば、深いコピーを作成する必要がある。
Pythonに標準添付のcopyモジュールには深いコピーを作成するためのdeepcopy関数がある。これを使うことで、深いコピーを作成できる。
以下にdeepcopy関数の呼び出し例を示す。
from copy import deepcopy
list_4 = deepcopy(list_2) # 深いコピー
print(list_2) # [[100, 1, 2], [3, 4, 5]]
print(list_4) # [[100, 1, 2], [3, 4, 5]]
ここでは先ほど浅いコピーで第0要素の第0要素が変更されたリストlist_2の深いコピーを作成している。
この画像にあるように、リストlist_4は深いコピーなので、コピー元のリストlist_2が含んでいた要素もコピーしたリストとなる。
そのため、ここでリストlist_4の第0要素の第0要素を変更しても、コピー元のリストlist_2にはその影響が及ばない。
list_4[0][0] = 0 # 深いコピーなら、list_4の要素を変更しても、list_2の要素は変更されない
print(list_4) # [[0, 1, 2], [3, 4, 5]]
print(list_2) # [[100, 1, 2], [3, 4, 5]]
こうした挙動が必要なときには、copyモジュールのdeepcopy関数を使うとよい。
参考:深いコピーを作成する関数を自分で実装する
深いコピーを作成する関数を自分で実装するには幾つかの点について考慮する必要がある。例えば、イミュータブル(変更不可能)なオブジェクトについては特に何も考えずに戻しても問題はない(変更できないので、上で見たような問題は生じない)。数値や文字列がこれに相当する。これらは変更できないオブジェクトなので、それらを変更しようとしたところで、コピー元のオブジェクトとの関連はなくなる。
以下を見てみよう。
list_00 = [0, 1, 2]
list_11 = deepcopy(list_00)
print(id(list_00[0])) # 4345661384
print(id(list_11[0])) # 4345661384
ここではdeepcopy関数でリストlist_00の深いコピーを作成して、リストlist_11に代入しているが、その第0要素のidは同じだ。だが、ここでどちらかのリストの第0要素の値を変更すると、その時点でその要素は別のオブジェクトを参照するようになるだけだ。そのため、一方への変更がもう一方に影響を及ぼすことはない。
ミュータブルなオブジェクトについては、それらについても深いコピーを再帰的に作成する必要がある。以下は、これを行う関数の例だ。ただし、循環参照には対応していない。
from collections.abc import Mapping, Sequence
# 循環参照を考慮しない深いコピー
def mydeepcopy(obj):
try: # ハッシュ可能(イミュータブル)なオブジェクトはそのまま返す
hash(obj)
return obj
except TypeError:
pass
obj_type = obj.__class__ # オブジェクトの型を取得
if isinstance(obj, Sequence): # シーケンスは各要素を再帰的にコピー
return obj_type(mydeepcopy(e) for e in obj)
if isinstance(obj, Mapping): # キーはそのまま、値を再帰的にコピー
tmp = {k: mydeepcopy(v) for k, v in obj.items()}
return obj_type(tmp)
return obj
ここではマッピングオブジェクト(辞書)などのキーについては、それらはハッシュ可能なもの(≒イミュータブル、変更不可能)であることから、深いコピーを作成しないようにしている。
実際にこのmydeepcopy関数を呼び出す例を以下に示す。
list_0 = [0, 1, 2]
list_1 = [3, 4, 5]
list_2 = [list_0, list_1]
list_3 = mydeepcopy(list_2)
print(list_2) # [[0, 1, 2], [3, 4, 5]]
print(list_3) # [[0, 1, 2], [3, 4, 5]]
list_3[0][0] = 100
print(list_2) # [[0, 1, 2], [3, 4, 5]]
print(list_3) # [[100, 1, 2], [3, 4, 5]]
単純なオブジェクトについてはこの程度の実装でも問題はなさそうだが、循環参照を含むリストについてはこの関数はRecursionError例外を発生する。
list_4 = [0]
list_4.append(list_4)
print(list_4) # [0, [...]]
list_5 = mydeepcopy(list_4) # RecursionError
ここではリストlist_4に自分自身をappendメソッドで追加し、その深いコピーを作成しようとしている。このときには、第1要素であるリストlist_4自身のコピーが永遠に終わらず、例外が発生している。
循環参照を考慮するにはメモなどの方法を用いて、自分自身をコピーしようとしている際にはコピーせずに、自分自身を返すようにするなどの手法が必要になる。これを行っているのが以下だ。
import array
def mydeepcopy(obj, memo=None):
if memo is None:
memo = {}
if id(obj) in memo:
return memo[id(obj)]
try: # ハッシュ可能(イミュータブル)なオブジェクトはそのまま返す
hash(obj)
return obj
except TypeError:
pass
obj_type = obj.__class__ # オブジェクトの型を取得
if isinstance(obj, tuple): # ミュータブルな要素を含むタプル
copied = obj_type(mydeepcopy(item, memo) for item in obj)
memo[id(obj)] = copied
return copied
if isinstance(obj, Sequence): # それ以外のシーケンス
if isinstance(obj, array.array):
copied = obj_type(obj.typecode)
else:
copied = obj_type()
memo[id(obj)] = copied
copied.extend(mydeepcopy(item, memo) for item in obj)
return copied
if isinstance(obj, Mapping): # キーはそのまま、値を再帰的にコピー
copied = obj_type()
memo[id(obj)] = copied
copied.update({k: mydeepcopy(v, memo) for k, v in obj.items()})
return copied
# その他のオブジェクト(集合など)はそのまま返す
memo[id(obj)] = obj
return obj
詳細な説明は割愛するが、ここでメモを辞書として用意して、コピー対象のオブジェクトのidをキーに、そのオブジェクトの複製を値としてメモに登録し、オブジェクトのコピーをする前にメモにそのidのオブジェクトが登録されていればコピーせずにそのオブジェクトを返すようにしている。その上で、タプルとそれ以外のシーケンス、マッピング(辞書)、その他のオブジェクトに処理を分けて、再帰的に要素をコピー(するか、オブジェクトをそのまま返送)している。
Pythonに標準添付のライブラリが提供するシーケンス(リスト、collections.dequeオブジェクト、arrya.arrayオブジェクトなど)では多くの場合、extendメソッドが備わっている。そのため、ここではメモに空のオブジェクトを登録した上で、そのオブジェクトをextendメソッドで変更することで、メモに登録したオブジェクトが再帰呼び出しの中で別のオブジェクトに置き換わることがないようにしている。そのため、extendメソッドを持たないシーケンスをコピーしようとすると例外が発生するはずだ。
マッピングオブジェクトについても、シーケンスと同様に、updateメソッドでメモに登録済みのオブジェクトを更新するようにして、再帰的なコピーの中でオブジェクトが入れ替わらないようにしている。
このコードは車輪の再発明をしている上に、極めて簡易的なものであり、クラスのインスタンスのコピーなどについては考慮していない。実用の観点からは素直にcopyモジュールのdeepcopy関数を使うことをお勧めする(copy.deepcopy関数の実装は上のコードとは全く異なっていて、より広範な種類のオブジェクトにまで対応しているはずだ)。
先ほどと同様なコードでこちらのmydeepcopy関数を呼び出す例を以下に示す。
list_4 = [0]
list_4.append(list_4)
print(list_4) # [0, [...]]
list_5 = mydeepcopy(list_4) # OK
print(list_5) # [0, [...]]
list_5[0] = 1
print(list_4) # [0, [...]]
print(list_5) # [1, [...]]
list_4[1][0] = 100
print(list_4) # [100, [...]]
print(list_5) # [1, [...]]
循環参照も(ある程度は)処理できているようだ。
Copyright© Digital Advantage Corp. All Rights Reserved.