note.nkmk.me

Pythonの浅いコピーと深いコピー: copy(), deepcopy()

Posted: 2021-08-22 / Modified: 2021-08-31 / Tags: Python

Pythonで浅いコピー(shallow copy)や深いコピー(deep copy)を生成するには、リストや辞書などのcopy()メソッドや、copyモジュールのcopy()関数、deepcopy()関数を使う。

ここでは以下の内容について説明する。

  • 浅いコピー(shallow copy)と深いコピー(deep copy)
  • 変数への代入
  • 浅いコピー(shallow copy): copy(), copy.copy()など
    • リストや辞書のcopy()メソッド
    • スライス
    • list(), dict()など
    • copy.copy()
  • 深いコピー(deep copy): copy.deepcopy()

別の変数への代入と浅いコピー、深いコピーの違いを最初にまとめておくと、以下のような結果となる。

import copy

l = [0, 1, [2, 3]]
l_assign = l                   # assignment
l_copy = l.copy()              # shallow copy
l_deepcopy = copy.deepcopy(l)  # deep copy

l[1] = 100
l[2][0] = 200
print(l)
# [0, 100, [200, 3]]

print(l_assign)
# [0, 100, [200, 3]]

print(l_copy)
# [0, 1, [200, 3]]

print(l_deepcopy)
# [0, 1, [2, 3]]
スポンサーリンク

浅いコピー(shallow copy)と深いコピー(deep copy)

公式ドキュメントの浅いコピー(shallow copy)と深いコピー(deep copy)の解説は以下の通り。

浅い (shallow) コピーと深い (deep) コピーの違いが関係するのは、複合オブジェクト (リストやクラスインスタンスのような他のオブジェクトを含むオブジェクト) だけです:

  • 浅いコピー (shallow copy) は新たな複合オブジェクトを作成し、その後 (可能な限り) 元のオブジェクト中に見つかったオブジェクトに対する 参照 を挿入します。
  • 深いコピー (deep copy) は新たな複合オブジェクトを作成し、その後元のオブジェクト中に見つかったオブジェクトの コピー を挿入します。
    copy --- 浅いコピーおよび深いコピー操作 — Python 3.9.4 ドキュメント

リストや辞書などのミュータブル(更新可能)オブジェクトの中のオブジェクト(= リストの要素や辞書の値value)に対して、浅いコピーは参照、深いコピーはコピーを挿入する。参照の場合は同一オブジェクトとなるので、オリジナルとコピー先の一方が更新されると他方も更新される。

例えば、リストのリスト(多次元リスト)や値valueとして辞書を含む入れ子になった辞書などでは、浅いコピーの場合、中身のリストや辞書を更新するともう一方も更新されてしまう。具体例は後述。

変数への代入

コピーの前に、変数に代入したときの動作を確認する。

リストや辞書などのミュータブル(更新可能)オブジェクトが代入された変数をさらに別の変数に代入した場合、いずれかの変数を更新(要素の変更や追加・削除など)すると他方も更新される。

l1 = [0, 1, [2, 3]]
l2 = l1

print(l1 is l2)
# True

l1[1] = 100
l1[2][0] = 200
print(l1)
# [0, 100, [200, 3]]

print(l2)
# [0, 100, [200, 3]]

print(l1 is l2)
# True

isの結果から分かるように、値を更新する前も後も2つの変数は同一のオブジェクトを指している。

中身が同じ別のオブジェクトを生成するには、後述のcopy()メソッドやcopy.deepcopy()関数などを使う。

一方、数値や文字列などのイミュータブル(更新不可能)オブジェクトの場合は、同一オブジェクトのまま値を更新できない。代入した時点では同一オブジェクトだが、新たな値に更新した時点で別のオブジェクトとなり、もう一方は元のまま。

i1 = 1
i2 = i1

print(i1 is i2)
# True

i1 += 100
print(i1)
# 101

print(i2)
# 1

print(i1 is i2)
# False

浅いコピー(shallow copy): copy(), copy.copy()など

リストや辞書のcopy()メソッド

リストや辞書などではcopy()メソッドが提供されている。copy()メソッドは浅いコピーを生成する。

上述の通り、浅いコピーでは元のオブジェクト中のオブジェクトの参照が挿入される。例えば、リストの浅いコピーの場合、リスト自体は異なるオブジェクトとなるが、その要素は同一オブジェクトを指す。

l = [0, 1, [2, 3]]
l_copy = l.copy()

print(l is l_copy)
# False

print(l[2] is l_copy[2])
# True

このため、要素がミュータブルの場合、オリジナルとコピー先の一方が更新されるともう一方も更新される。イミュータブルの場合は、新たな値に更新した時点で別のオブジェクトとなるため、もう一方は元のまま。

l[1] = 100
l[2][0] = 200
print(l)
# [0, 100, [200, 3]]

print(l_copy)
# [0, 1, [200, 3]]

print(l[2] is l_copy[2])
# True

例はリストを含むリストだが、辞書のリストや入れ子になった辞書(辞書の辞書)などでも同様。また、組み込み型に限らず、独自に定義したクラスに対しても同様。

オブジェクト中のオブジェクトも別のオブジェクトとする深いコピーを生成するには後述のcopy.deepcopy()を使う。

スライス

リストなどのミュータブルなシーケンス型に対するスライスも浅いコピーを生成する。

例えば、全体を示すスライス[:]を適用すると、リスト全体の浅いコピーが生成される。

l = [0, 1, [2, 3]]
l_whole_slice = l[:]

l[1] = 100
l[2][0] = 200
print(l)
# [0, 100, [200, 3]]

print(l_whole_slice)
# [0, 1, [200, 3]]

ミュータブルなシーケンス型にcopy()メソッドが追加されたのはPython3.3からなので、それより前は[:]で浅いコピーを生成するテクニックが使われていた。これから新たに書くコードではcopy()メソッドを使ったほうが意図が明確になるだろう。

一部分に対するスライスでも同様。浅いコピーが生成される。

l = [0, 1, [2, 3]]
l_slice = l[1:]
print(l_slice)
# [1, [2, 3]]

l[1] = 100
l[2][0] = 200
print(l)
# [0, 100, [200, 3]]

print(l_slice)
# [1, [200, 3]]

深いコピーを生成したい場合はスライスに対してcopy.deepcopy()関数を適用すればよい。

list(), dict()など

list()にリストを渡したり、dict()に辞書を渡すことでリストや辞書を生成できるが、このとき生成されるのも浅いコピー。

l = [0, 1, [2, 3]]
l_list = list(l)

l[1] = 100
l[2][0] = 200
print(l)
# [0, 100, [200, 3]]

print(l_list)
# [0, 1, [200, 3]]

copy.copy()

copyモジュールのcopy()関数で浅いコピーを生成することも可能。

l = [0, 1, [2, 3]]
l_copy = copy.copy(l)

l[1] = 100
l[2][0] = 200
print(l)
# [0, 100, [200, 3]]

print(l_copy)
# [0, 1, [200, 3]]

リストや辞書などではcopy()メソッドが提供されているのでそれを使えばよいが、独自に定義したクラスなどcopy()メソッドが定義されていないオブジェクトの浅いコピーを生成したい場合はcopy.copy()を使う。

深いコピー(deep copy): copy.deepcopy()

深いコピーを生成するには、copyモジュールのdeepcopy()関数を使う。

l = [0, 1, [2, 3]]
l_deepcopy = copy.deepcopy(l)

print(l is l_deepcopy)
# False

print(l[2] is l_deepcopy[2])
# False

l[1] = 100
l[2][0] = 200
print(l)
# [0, 100, [200, 3]]

print(l_deepcopy)
# [0, 1, [2, 3]]

深いコピーはオブジェクト中のオブジェクトがオリジナルとコピー先で別のオブジェクトとして扱われる。ミュータブルであっても、一方を更新しても他方が更新されることはない。

一部分に対するスライスにdeepcopy()関数を適用する例は以下の通り。

l = [0, 1, [2, 3]]
l_slice_deepcopy = copy.deepcopy(l[1:])
print(l_slice_deepcopy)
# [1, [2, 3]]

l[1] = 100
l[2][0] = 200
print(l)
# [0, 100, [200, 3]]

print(l_slice_deepcopy)
# [1, [2, 3]]
スポンサーリンク
シェア
このエントリーをはてなブックマークに追加

関連カテゴリー

関連記事