🐍

b = a と b = a[:] の違いを説明できますか?

に公開1

1. はじめに

Python でリストや他のオブジェクトを複製するとき、
b = ab = a[:] には大きな違いがあります。
まずはその違いを押さえたうえで、コピーの深さによって挙動がどう変わるのかを整理しましょう。

🎯 先に結論:b = ab = a[:] の違い

記述 動作 変更の影響 メモリ効率
b = a 参照渡しba は同じリストを指す 片方を変更するともう片方も変わる ◎(コピーなし)
b = a[:] 浅いコピー – 新しいリストオブジェクトを作成 第一層の変更は独立。入れ子要素は共有 ◯(リスト本体のみ複製)

📌 まず用語の整理

用語 意味 元データとの関係
参照渡し (reference) 元データへの参照のみ渡す。複製はしない。 常に影響を受ける
浅いコピー (shallow copy) 表面(第一層)だけを複製。内部の入れ子データまでは複製しない。 第一層までは影響なし、入れ子データは影響を受ける
深いコピー (deep copy) すべての階層を完全に複製。 一切影響を受けない

2. 参照渡し(Reference)

変数はデータそのものではなく、
データが存在するメモリ位置へのポインタ(参照) を保持します。

a = [1, 2, 3]
b = a          # 参照渡し

b[0] = 100
print(a)  # [100, 2, 3]  ← a も変わる
print(b)  # [100, 2, 3]

メモリイメージ:

a ──┐
    ├─▶ [1, 2, 3]
b ──┘    (同じメモリ領域を共有)

特徴

  • メモリ効率が良い(複製しない)
  • 片方を変更するともう片方にも影響が及ぶ

3. 浅いコピー(Shallow Copy)

最上位層だけをコピーし、
入れ子要素は元データと共有 します。

import copy

a = [1, [2, 3], 4]
b = copy.copy(a)   # または a[:] でも可

b[0] = 100         # 第一層の変更
b[1][0] = 200      # 入れ子要素の変更

print("a:", a)  # [1, [200, 3], 4] ← 入れ子要素は影響あり
print("b:", b)  # [100, [200, 3], 4]

メモリイメージ:

a ──▶ [ 1  , ──▶ [2, 3] , 4 ]
           ↑          ↑
b ──▶ [100 , ──▶ [2, 3] , 4 ]

特徴

  • 第一層の変更は独立
  • 入れ子要素の変更は元データに波及

4. 深いコピー(Deep Copy)

オブジェクトとその中身を 再帰的にすべて複製 します。

import copy

a = [1, [2, 3], 4]
b = copy.deepcopy(a)

b[0] = 100          # 第一層の変更
b[1][0] = 200       # 入れ子要素の変更

print("a:", a)  # [1, [2, 3], 4] ← 一切影響なし
print("b:", b)  # [100, [200, 3], 4]

メモリイメージ:

a ──▶ [ 1   , ──▶ [2  , 3] , 4 ]
b ──▶ [100  , ──▶ [200, 3] , 4 ]   (完全に独立)

特徴

  • 変更が元データに波及しない
  • メモリと処理コストは大きい

5. まとめ表

種類 コピー範囲 メモリ効率 元データへの影響
参照渡し コピーしない(参照のみ) あり
浅いコピー 第一層のみ 入れ子要素は影響あり
深いコピー すべてをコピー なし

6. 利用指針(いつ何を使うべき?)

  • 参照渡し

    • メモリを節約したいとき
    • 同じデータを共有し、変更が相互に反映されてよい場合
  • 浅いコピー

    • 入れ子構造がない、または内部要素がイミュータブルなとき
    • トップレベルだけ独立させたい場合
  • 深いコピー

    • ネストが深い複雑なデータを完全に分離したい場合
    • 変更が元データに波及しては困る場合

7. 実用的ヒント

  • 単純な1次元リスト ➡︎ 浅いコピー (a[:] または copy.copy(a))
  • 複雑な入れ子構造 ➡︎ 深いコピー (copy.deepcopy(a))

9. 専門家向け補足

9‑1. [:]list.copy() の違いは?

a_copy1 = a[:]          # ビルトインのスライス処理 (C 実装)
a_copy2 = a.copy()      # CPython 3.3 以降のメソッド

両者とも O(n) で 浅いコピー を返しますが、list.copy()

  • 型ヒントの自動補完が利く
  • 既存コードの「スライス誤用」を減らせる
    という観点で PEP 3137 によって追加されました。速度差はほぼありません(CPython ではどちらも PyList_GetSlice へフォールバック)。

9‑2. copy.copy() / copy.deepcopy() の内部

関数 実装フロー 上書きフック
copy.copy(x) 1. type(x).__copy__(x) があれば呼ぶ
2. __reduce_ex__ / __reduce__ でシリアライズ→復元
__copy__
copy.deepcopy(x) 再帰的に 1.を実行し、メモ化辞書で循環参照を検知 __deepcopy__(memo)
  • 循環参照 がある場合、deepcopy()同一 ID のオブジェクトをメモ化辞書で共有し、無限再帰を防ぎます。
  • カスタムクラスで高パフォーマンスを狙うなら、必要最低限の属性だけをコピーする __getstate__ / __setstate__ を実装すると高速化できます。

9‑3. ビュー (view) とは何か ― NumPy を例に

ビューとは「実体データをコピーせずに、同じメモリ領域を “別の窓” から参照するオブジェクト」です。
NumPy ではスライスや転置などで自動的にビューが返されることが多く、コピーとの違いを理解していないと意図せず元データを壊す原因になります。

import numpy as np

x = np.arange(5)      # 実体データ [0 1 2 3 4]
y = x[1:4]            # ビュー(コピー無し)
z = x.copy()          # 完全コピー
変数 共有メモリ 例:y[0] = 99 の影響 説明
x - - オリジナル
y はい x も変わる スライスなのでビュー
z いいえ x は変わらない copy() で確保した独立バッファ

ビューが返る代表的な操作

操作 典型例 振る舞い
スライス x[a:b] 連続メモリならビュー
転置 x.T 軸情報を書き換えるだけ
reshape x.reshape(-1, 1) 形状だけ再計算
ストライド変更 x[::2] ステップ幅を変えるだけ

Note Fancy Indexing(配列でインデックスを渡す方法)は非連続アクセスになるため 必ずコピー が返ります。

ビュー vs コピー — メリット / デメリット

項目 ビュー コピー
メモリ追加 ゼロ 要素数 × 型サイズ
生成コスト O(1) O(n)
元データへの波及 あり なし
安全性 管理が必要 高い

いつ copy() を取るか?

  • 学習用テンソルと検証用テンソルを完全分離したい場合
  • スレッド / 並列計算で データ競合 を防ぎたい場合
  • NumPy → C / CUDA バッファへポインタを渡すときに 不変保証 が求められる場合

まとめると、「ビューは高速 & 省メモリだが可変オブジェクトと扱うときは要注意」 というのが実践的な指針です。

9‑4. COW (Copy‑on‑Write) の発想

Pandas・PyTorch など一部ライブラリは COW を採用し、

参照カウント >1 になった瞬間だけ実体コピー
という戦略でメモリ効率と安全性を両立させています。
Python 標準のリストには組み込まれていないため、自前で COW を導入したい場合は proxy クラスを噛ませて __getitem__/__setitem__ をフックする必要があります。

9‑5. 共有参照がバグになる典型パターン

def append_sentinel(lst, item=[]):  # ← NG!
    item.append(lst)
    return item
  • デフォルト引数は 関数定義時に 1 度だけ 評価されるため、item は常に同一リストを共有します。
  • 対策:None 判定で初期化するイディオムを徹底しましょう。
def append_sentinel(lst, item=None):
    if item is None:
        item = []
    item.append(lst)
    return item

8. 🎯 最後のまとめ

  • Python の変数は参照を保持する
  • コピーの深さで元データとの依存関係が変わる
  • シーンに合わせて「参照」「浅いコピー」「深いコピー」を正しく使い分ける

これらを理解すれば、予期せぬ副作用を防ぎ、安全で効率的なコードを書くことができます。

Discussion

YuneKichiYuneKichi

参照渡し (Call by Reference) は、関数の引数の取り扱いにおいて使う用語ですね (対義語:値渡し/Call by Value)。
そもそも参照渡しができないPythonでは使わない用語かもしれませんが……。