b = a と b = a[:] の違いを説明できますか?
1. はじめに
Python でリストや他のオブジェクトを複製するとき、
b = a
と b = a[:]
には大きな違いがあります。
まずはその違いを押さえたうえで、コピーの深さによって挙動がどう変わるのかを整理しましょう。
b = a
と b = a[:]
の違い
🎯 先に結論:記述 | 動作 | 変更の影響 | メモリ効率 |
---|---|---|---|
b = a |
参照渡し – b と a は同じリストを指す |
片方を変更するともう片方も変わる | ◎(コピーなし) |
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. 専門家向け補足
[:]
と list.copy()
の違いは?
9‑1. a_copy1 = a[:] # ビルトインのスライス処理 (C 実装)
a_copy2 = a.copy() # CPython 3.3 以降のメソッド
両者とも O(n) で 浅いコピー を返しますが、list.copy()
は
- 型ヒントの自動補完が利く
- 既存コードの「スライス誤用」を減らせる
という観点で PEP 3137 によって追加されました。速度差はほぼありません(CPython ではどちらもPyList_GetSlice
へフォールバック)。
copy.copy()
/ copy.deepcopy()
の内部
9‑2. 関数 | 実装フロー | 上書きフック |
---|---|---|
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
参照渡し (Call by Reference) は、関数の引数の取り扱いにおいて使う用語ですね (対義語:値渡し/Call by Value)。
そもそも参照渡しができないPythonでは使わない用語かもしれませんが……。