Python リストの浅いコピーと深いコピーの違い

## はじめに
Pythonでプログラミングを行う上で、データ構造の操作は避けて通れません。特に、リストのコピーは頻繁に行われる操作の一つです。しかし、「コピー」という一見単純な操作にも、実は重要な違いがあります。それが「浅いコピー（Shallow Copy）」と「深いコピー（Deep Copy）」です。

この記事では、これらの違いを詳しく解説し、実際のコード例を通じて理解を深めていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/1a160eb7-5862-2054-d9f8-27034b30e116.png)

## 浅いコピーと深いコピーの比較

まずは、浅いコピーと深いコピーの主な違いを表で比較してみましょう。

| 特徴 | 浅いコピー (Shallow Copy) | 深いコピー (Deep Copy) |
|------|---------------------------|------------------------|
| 実行方法 | `list.copy()` または `copy.copy()` | `copy.deepcopy()` |
| コピーの深さ | 最上位の要素のみ | すべての階層を再帰的に |
| 処理速度 | 高速 | 低速（特に大きなオブジェクトや深い階層の場合） |
| メモリ使用量 | 少ない | 多い（元のオブジェクトの完全なコピーを作成） |
| ネストされた要素の扱い | 参照をコピー | 新しいオブジェクトとしてコピー |
| ~~ミュータブル要素への影響~~ | ~~元のオブジェクトの変更が影響する~~ | ~~完全に独立したコピー~~ |
| ~~イミュータブル要素への影響~~ | ~~影響なし~~ | ~~影響なし~~ |
| 循環参照の処理 | 処理しない | 適切に処理する |
| カスタマイズ方法 | `__copy__` メソッドをオーバーライド | `__deepcopy__` メソッドをオーバーライド |
| 適した使用場面 | 単純な構造や一時的な複製 | 複雑な構造や完全な独立性が必要な場合 |

この表を見ると、両者には明確な違いがあることがわかります。では、実際のコードを通じてこれらの違いを詳しく見ていきましょう。

## 基本的な違い

まずは、最も基本的な例から見ていきます。

```python
import copy

original = [1, [2, 3], 4]
shallow_copy = original.copy()
deep_copy = copy.deepcopy(original)

original[1][0] = 'X'

print("元のリスト:", original)
print("浅いコピー:", shallow_copy)
print("深いコピー:", deep_copy)
```

出力:
```
元のリスト: [1, ['X', 3], 4]
浅いコピー: [1, ['X', 3], 4]
深いコピー: [1, [2, 3], 4]
```

この結果から、以下のことがわかります：

1. 浅いコピーでは、内部リスト `[2, 3]` への参照がコピーされるため、元のリストの変更が反映されます。
2. 深いコピーでは、完全に新しいオブジェクトが作成されるため、元のリストの変更は反映されません。

## パフォーマンスとメモリ使用量

次に、パフォーマンスとメモリ使用量の違いを見てみましょう。

```python
import sys
import copy
import time

def measure_copy_performance(data):
    start = time.time()
    shallow = data.copy()
    shallow_time = time.time() - start

    start = time.time()
    deep = copy.deepcopy(data)
    deep_time = time.time() - start

    print(f"浅いコピーの時間: {shallow_time:.6f}秒")
    print(f"深いコピーの時間: {deep_time:.6f}秒")
    print(f"元のデータのサイズ: {sys.getsizeof(data)} bytes")
    print(f"浅いコピーのサイズ: {sys.getsizeof(shallow)} bytes")
    print(f"深いコピーのサイズ: {sys.getsizeof(deep)} bytes")

# 大きなネストされたリストでテスト
big_data = [[i for i in range(1000)] for _ in range(100)]
measure_copy_performance(big_data)
```

出力例:
```
浅いコピーの時間: 0.000015秒
深いコピーの時間: 0.015625秒
元のデータのサイズ: 904736 bytes
浅いコピーのサイズ: 904736 bytes
深いコピーのサイズ: 904736 bytes
```

この結果から、以下のことが分かります：

1. 浅いコピーは深いコピーよりも大幅に高速です。
2. `sys.getsizeof()` では差が見えませんが、実際には深いコピーはより多くのメモリを使用しています。これは、Pythonのリスト実装の特性によるものです。

## イミュータブルとミュータブル

~~Pythonのオブジェクトがイミュータブル（変更不可）かミュータブル（変更可能）かによって、コピーの挙動が変わります。~~
Rev.1: コメント欄の指摘の通り記述に間違いがあり、取り消し線してます。
指摘事項の内容の詳細はコメント欄を参照ください。

```python
# イミュータブルな要素のみのリスト
immutable_list = [1, 2, 3, "string", (4, 5)]
shallow_immutable = immutable_list.copy()

# ミュータブルな要素を含むリスト
mutable_list = [1, [2, 3], {"key": "value"}]
shallow_mutable = mutable_list.copy()

immutable_list[0] = 100  # この変更は shallow_immutable には影響しない
mutable_list[1][0] = 200  # この変更は shallow_mutable にも影響する

print("Immutable変更後:", immutable_list, shallow_immutable)
print("Mutable変更後:", mutable_list, shallow_mutable)
```

出力:
```
Immutable変更後: [100, 2, 3, 'string', (4, 5)] [1, 2, 3, 'string', (4, 5)]
Mutable変更後: [1, [200, 3], {'key': 'value'}] [1, [200, 3], {'key': 'value'}]
```

~~この結果から、以下のことが分かります：~~

~~1. イミュータブルな要素のみのリストでは、浅いコピーでも変更の影響を受けません。~~
~~2. ミュータブルな要素を含むリストでは、浅いコピーが元のリストの変更の影響を受けます。~~

## カスタムクラスとコピー

カスタムクラスを使用する場合、`copy`モジュールの動作をカスタマイズできます。

```python
class CustomList:
    def __init__(self, data):
        self.data = data

    def __copy__(self):
        print("__copy__メソッドが呼ばれました")
        return CustomList(self.data.copy())

    def __deepcopy__(self, memo):
        print("__deepcopy__メソッドが呼ばれました")
        return CustomList(copy.deepcopy(self.data, memo))

    def __repr__(self):
        return f"CustomList({self.data})"

obj = CustomList([1, [2, 3]])
shallow = copy.copy(obj)
deep = copy.deepcopy(obj)

obj.data[1][0] = 'X'
print("元のオブジェクト:", obj)
print("浅いコピー:", shallow)
print("深いコピー:", deep)
```

出力:
```
__copy__メソッドが呼ばれました
__deepcopy__メソッドが呼ばれました
元のオブジェクト: CustomList([1, ['X', 3]])
浅いコピー: CustomList([1, ['X', 3]])
深いコピー: CustomList([1, [2, 3]])
```

この例から、カスタムクラスで `__copy__` と `__deepcopy__` メソッドを適切に実装することで、期待通りの浅いコピーと深いコピーの動作を実現できることがわかります。

## 循環参照の処理

最後に、循環参照を含む構造でのコピーの挙動を見てみましょう。

```python
class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

    def __repr__(self):
        return f"Node({self.data})"

# 循環リストの作成
a = Node('A')
b = Node('B')
c = Node('C')
a.next = b
b.next = c
c.next = a  # 循環参照

# deepcopy は循環参照を適切に処理できる
deep_copy = copy.deepcopy(a)
print(f"オリジナル: {a} -> {a.next} -> {a.next.next} -> {a.next.next.next}")
print(f"ディープコピー: {deep_copy} -> {deep_copy.next} -> {deep_copy.next.next} -> {deep_copy.next.next.next}")
```

出力:
```
オリジナル: Node(A) -> Node(B) -> Node(C) -> Node(A)
ディープコピー: Node(A) -> Node(B) -> Node(C) -> Node(A)
```

この結果から、`copy.deepcopy()` が循環参照を適切に処理し、元のオブジェクト構造を維持したコピーを作成できることがわかります。

## まとめ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/1f7c4b3f-d252-f07a-642e-6b59abe42e11.png)

Pythonにおけるリストの浅いコピーと深いコピーについて、以下のポイントを押さえておきましょう：

1. 浅いコピーは高速でメモリ効率が良いですが、ネストされたミュータブルオブジェクトの変更に注意が必要です。
2. 深いコピーは完全に独立したコピーを作成しますが、パフォーマンスコストが高くなります。
3. イミュータブルなオブジェクトに対しては、浅いコピーでも深いコピーでも結果は同じです。
4. カスタムクラスでは `__copy__` と `__deepcopy__` メソッドをオーバーライドしてコピー動作をカスタマイズできます。
5. 循環参照を含む複雑なデータ構造では、`copy.deepcopy()` が適切に処理できます。

リストのコピーは一見単純に見えますが、実際のアプリケーション開発では様々な考慮事項があります。状況に応じて適切なコピー方法を選択し、潜在的な問題を回避することが重要です。この記事で学んだ知識を活かし、より効率的で安全なPythonコードを書いていきましょう。