Python | データ構造強化:リストのシャローコピー

Python Python
スポンサーリンク

概要(シャローコピーは「外側だけ新しく、中身は同じものを指す」コピー)

リストのシャローコピー(浅いコピー)は、リストそのもの(外側の入れ物)だけを新しく作り、要素がオブジェクトならその参照をそのままコピーします。数値や文字列のようなイミュータブルは実質影響しづらい一方、入れ子のリストなどミュータブル要素は「元とコピーで中身が共有」されます。これが“思ったより連動してしまう”原因です。シャローコピーの作り方は複数あり、用途に応じて最短の書き方を選びます。

# 代表的なシャローコピーの作り方
a = [1, 2, 3]
b = a[:]            # スライス
c = list(a)         # コンストラクタ
d = a.copy()        # メソッド
import copy
e = copy.copy(a)    # モジュール
Python

基本の使い方(ここが重要)

「= はコピーではなく参照の代入」から押さえる

代入演算子「=」は、同じリストを別名で指すだけです。片方の変更がもう片方にも反映されます。まずここを誤らないことが最重要です。

x = [1, 2, 3]
y = x          # コピーのつもりでも、同じものを指す
y[0] = 100
print(x)       # [100, 2, 3] 連動してしまう
Python

シャローコピーは「入れ物は別物、中身の参照は同じ」

外側だけが新しくなり、要素への参照は共有されます。イミュータブルは代入で“置き換え”になるので共有の問題が表面化しにくく、ミュータブル要素は中身の変更が双方に波及します。

a = [[1, 2], [3, 4]]
b = a[:]                # シャローコピー
b[0].append(99)         # 内側リストを“変更”
print(a)                # [[1, 2, 99], [3, 4]] 共有されているため変わる
print(b is a, b[0] is a[0])  # False, True(外側は別、中は同じ)
Python

重要ポイントの深掘り(方法の違い・入れ子構造・防御的コピー)

代表的な作り方の違いと選び方

スライス a[:] は最短で、list(a) は可読性が高く、a.copy() はリストであることが明示的です。copy.copy(a) は「シャローを明言」でき、型がリスト以外に変わっても同じ意図で使えます。どれも「外側1階層だけ新しくする」点は同じです。

a = [1, 2, 3]
b = a[:]
c = list(a)
d = a.copy()
import copy
e = copy.copy(a)
Python

入れ子で“罠”になる理由と確認方法

内側のオブジェクトが共有されるため、変更が伝播します。id() で参照が同じか確認すると、仕組みを掴めます。

a = [[1], [2]]
b = a.copy()
print(id(a), id(b))          # 外側は別
print(id(a[0]), id(b[0]))    # 内側は同じ → 共有
Python

防御的コピー(必要な階層だけ手動でコピー)

深いコピーが重い・不要なときは、「変わりうる階層だけ」自分でコピーします。内側が一次元のときは内側もシャローコピーして独立させるのが現実的です。

a = [[1, 2], [3, 4]]
# 内側も“1階層だけ”コピーして防御
b = [inner[:] for inner in a]
b[0].append(99)
print(a)  # 影響しない
Python

完全に独立させたいならディープコピー

入れ子が多段のときや、共有による副作用を完全に排除したいときは deepcopy を使います。ただしコストは上がります。

import copy
a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)
b[0].append(99)
print(a)  # 変わらない
Python

実務の勘所(使い分け・性能・関数引数)

どれを使うべきかの指針

入れ子がない(または中身を変更しない)ならシャローコピーで十分です。入れ子があり、内側を変える可能性があるなら「防御的コピー」か deepcopy。コレクションが大きい場合、深いコピーは時間とメモリを食うため、必要最小限の階層だけコピーする方が現実的です。

性能の見方(シャローは軽い、ディープは重い)

シャローコピーは外側だけ作るため高速で軽量です。ディープコピーは再帰的に辿るため、サイズや階層に比例してコストが増えます。テストで計測し、必要な範囲に留めましょう。

関数引数での「思わぬ連動」を避ける

引数で受け取ったリストを関数内で破壊的に変更するなら、意図的にシャローコピーを取ってから操作すると安全です。呼び出し側のデータを守れます。

def safe_process(xs):
    ys = xs[:]          # 防御的コピー
    ys.append("done")
    return ys

original = [1, 2]
result = safe_process(original)
print(original)  # 変わらない
Python

例題で身につける(定番から一歩先まで)

例題1:スライス・list・copy の等価性を確認

a = [1, 2, 3]
b, c, d = a[:], list(a), a.copy()
b[0] = 99
print(a, b, c, d)  # a は変わらない、b/c/d は別物
Python

例題2:入れ子リストでの共有の可視化

a = [[1, 2], [3, 4]]
b = a.copy()
b[1].remove(4)
print("a:", a)  # [[1, 2], [3]] 共有のため影響
print("b:", b)  # [[1, 2], [3]]
Python

例題3:一次入れ子だけ守る防御的コピー

a = [[1, 2], [3, 4]]
b = [row.copy() for row in a]  # row[:] でも同じ
b[0].append(99)
print(a)  # [[1,2],[3,4]]
print(b)  # [[1,2,99],[3,4]]
Python

例題4:設定テンプレートを安全に加工(必要階層のみコピー)

template = {"path": ["in", "out"], "opts": {"mode": "fast", "retry": 2}}
# ここでは“pathリストだけ”変更したい → その階層だけコピー
cfg = dict(template)               # 外側はシャロー
cfg["path"] = list(template["path"])  # 該当階層を独立
cfg["path"].append("tmp")
print(template["path"])  # 影響しない
Python

まとめ

シャローコピーは「外側だけ新しくして、中身の参照は共有する」コピーです。= はコピーではなく参照の代入、シャローは a[:], list(a), a.copy(), copy.copy(a) で作れる。入れ子のミュータブル要素は共有されるため、変更が伝播します。必要な階層だけ手動でコピーする防御的コピー、完全独立なら deepcopy を使い分ける。性能と安全性のバランスを取り、関数引数やテンプレート加工では“意図的なコピー”を癖にすると、予期せぬ連動バグを避けて、読みやすく堅牢なコードになります。

タイトルとURLをコピーしました