前提
Pythonの変数は「オブジェクトへの参照(ポインタ)」を保持しています。
つまり、変数はオブジェクトの位置情報を持っているということです。
変数Aが存在する時、変数B=変数Aとしても、変数Bはオブジェクトへの参照しか保持しません。
この点を理解しておくと、この記事の理解がしやすいと思います。
何も考えない場合にどうなるか
結論、一方の変数の値を更新すると、もう一方の変数も更新されます。
なぜなら、各変数が同じオブジェクトを参照するからです。
A = [1, 2, 3]
B = A # 単なる参照の代入
print(f"A: {A}")
print(f"B: {B}")
# 出力
A: [1, 2, 3]
B: [1, 2, 3]
ここで、A
に任意の値を代入します。
A[0] = 100
print(f"A: {A}")
print(f"B: {B}")
A[0][0] = 100
によって、元のオブジェクトが更新されました。
出力を見てみましょう。
# 出力 ※Bが書き換わっていることに注目
A: [100, 2, 3]
B: [100, 2, 3]
このように、変数A
と変数B
はオブジェクトのポインタ(場所)を保持しているだけなので、値がどちらも更新されます。
Bからオブジェクトを更新する場合も同様です。
B[1] = 200
print(f"A: {A}")
print(f"B: {B}")
# 出力 ※Aが書き換わっていることに注目
A: [100, 200, 3]
B: [100, 200, 3]
浅いコピー(shallow copy)
ここまでみてきた問題の本質は、複数の変数が同じオブジェクトを参照していることです。
つまり、オブジェクトを新規作成して、変数ごとに別のオブジェクトを参照することが解決の糸口です。
オブジェクトの新規をするためには浅いコピーを使います。
import copy
A = [1, 2, 3]
B = copy.copy(A) # 浅いコピー
print(f"A: {A}")
print(f"B: {B}")
# 出力(ここの結果は同じ)
# A: [1, 2, 3]
# B: [1, 2, 3]
ここから変わります。
A[0]
を100
に書き換えた場合、新規オブジェクトを作成するため、Aのみ値が書き変わります。
A[0] = 100
print(f"A: {A}")
print(f"B: {B}")
# 出力
# A: [100, 2, 3]
# B: [1, 2, 3]
B[1]
を200
に書き換えた場合は、Bのみ更新されます
B[1] = 200
print(f"A: {A}")
print(f"B: {B}")
# 出力
# A: [100, 2, 3]
# B: [1, 200, 3]
みゅーたぶる、いみゅーたぶる?(飛ばしても構いません)
余裕のある方は、一度視点を広げましょう。
ここまでみてきた方の中に以下のようなことを思った方もいるのではないでしょうか。
「コピーとか言ってるけど、文字列とかの場合、上記のような問題は起きないよ?」
これは私も行き着いた疑問です。
ここで、ミュータブルとイミュータブルについて触れます。
ミュータブル:変更できるオブジェクト(list, dict, setなど)
イミュータブル:変更できないオブジェクト(str, int, tupleなど)
どういうこと?
仮に変数A = hoge
という変数を保持していて、値を fuga
に書き換えた場合、以下のようになります。
A = 'hoge'
B = A # Bは'hoge'を参照
A = 'fuga' # Aは新しいオブジェクト'fuga'を参照
print(f"A: {A}")
print(f"B: {B}")
print(id(A) == id(B))
# 出力
# A: fuga
# B: hoge
# False # Bはまだ'hoge'を参照
以上のことから、イミュータブルは値の更新ができないという特性上、変数の値が書き換えられるという事象が起きないのです。
浅いコピーの限界
ここが私のハマったポイントです。
以下、Python公式より
浅いコピー (shallow copy) は新たな複合オブジェクトを作成し、その後 (可能な限り) 元のオブジェクト中に見つかったオブジェクトに対する 参照 を挿入します。
つまり、最上位リストの中のリストは変数間でオブジェクトを共有します。
これを意識しないと以下のような問題が起きます。
# リストの中にリストを持たせたデータ構造
A = [[1,2,3], [4,5,6], [7,8,9]]
B = copy.copy(A) # 浅いコピー
print(f"A: {A}")
print(f"B: {B}")
# 出力
# A: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
# B: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
浅いコピーは最上位のリストを置き換える場合、新規オブジェクトを作成するため、変数間でオブジェクトの共有がされません。
A[0] = [100, 101]
print(f"A: {A}")
print(f"B: {B}")
# 出力
# A: [[100, 101], [4, 5, 6], [7, 8, 9]]
# B: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
A[0]
のリストは新規作成したオブジェクトのため、値の更新が他の変数に影響しません。
A[0][0] = 200
print(f"A: {A}")
print(f"B: {B}")
# 出力
# A: [[200, 101], [4, 5, 6], [7, 8, 9]]
# B: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
問題のケース
リスト内のリストを直接更新するケースではオブジェクトを変数間で共有しているため問題が各変数の値が更新されてしまいます。
A[1][0] = 200
print(f"A: {A}")
print(f"B: {B}")
# 出力(※B[1][0]も更新されている)
# A: [[100, 101], [200, 5, 6], [7, 8, 9]]
# B: [[1, 2, 3], [200, 5, 6], [7, 8, 9]]
深いコピー(deep copy)
この問題を解消するのが、深いコピーです。
以下、Python公式より
深いコピー (deep copy) は新たな複合オブジェクトを作成し、その後元のオブジェクト中に見つかったオブジェクトの コピー を挿入します。
深いコピー操作には、しばしば浅いコピー操作の時には存在しない 2 つの問題がついてまわります:
再帰的なオブジェクト (直接、間接に関わらず、自分自身に対する参照を持つ複合オブジェクト) は再帰ループを引き起こします。
深いコピーは何もかもコピーしてしまうため、例えば複数のコピー間で共有するつもりだったデータも余分にコピーしてしまいます。
つまり、ネストされたリストのオブジェクトも更新します。
ただし、深いネストや、その一部を変数間で共有している場合などは注意が必要です。
import copy
A = [[1,2,3], [4,5,6], [7,8,9]]
B = copy.deepcopy(A) # 深いコピー
A[1][0] = 200
print(f"A: {A}")
print(f"B: {B}")
# 出力
# A: [[1, 2, 3], [200, 5, 6], [7, 8, 9]]
# B: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
deepcopyは小規模でシンプルなデータ構造の場合、割とシンプルに考えられると思います。
ただし、大規模なデータ管理する場合、メモリ容量などを考慮する必要があるので、浅いコピーと深いコピーは理解しておいた方がいろんな局面で応用が効きそうです。
あとがき
AtCoderのコンテスト中に軽くハマったのでちゃんと理解して記事にしてみました。
データをどう切り取ってどう扱うか、問題文(自然言語)をソースに落とし、正確に実装するという点で、一定のレベルまではエンジニアとsの基礎力が養えると改めて感じました。