はじめに
Pythonでプログラミングを行う上で、データ構造の操作は避けて通れません。特に、リストのコピーは頻繁に行われる操作の一つです。しかし、「コピー」という一見単純な操作にも、実は重要な違いがあります。それが「浅いコピー(Shallow Copy)」と「深いコピー(Deep Copy)」です。
この記事では、これらの違いを詳しく解説し、実際のコード例を通じて理解を深めていきます。
浅いコピーと深いコピーの比較
まずは、浅いコピーと深いコピーの主な違いを表で比較してみましょう。
特徴 | 浅いコピー (Shallow Copy) | 深いコピー (Deep Copy) |
---|---|---|
実行方法 |
list.copy() または copy.copy()
|
copy.deepcopy() |
コピーの深さ | 最上位の要素のみ | すべての階層を再帰的に |
処理速度 | 高速 | 低速(特に大きなオブジェクトや深い階層の場合) |
メモリ使用量 | 少ない | 多い(元のオブジェクトの完全なコピーを作成) |
ネストされた要素の扱い | 参照をコピー | 新しいオブジェクトとしてコピー |
循環参照の処理 | 処理しない | 適切に処理する |
カスタマイズ方法 |
__copy__ メソッドをオーバーライド |
__deepcopy__ メソッドをオーバーライド |
適した使用場面 | 単純な構造や一時的な複製 | 複雑な構造や完全な独立性が必要な場合 |
この表を見ると、両者には明確な違いがあることがわかります。では、実際のコードを通じてこれらの違いを詳しく見ていきましょう。
基本的な違い
まずは、最も基本的な例から見ていきます。
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]
この結果から、以下のことがわかります:
- 浅いコピーでは、内部リスト
[2, 3]
への参照がコピーされるため、元のリストの変更が反映されます。 - 深いコピーでは、完全に新しいオブジェクトが作成されるため、元のリストの変更は反映されません。
パフォーマンスとメモリ使用量
次に、パフォーマンスとメモリ使用量の違いを見てみましょう。
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
この結果から、以下のことが分かります:
- 浅いコピーは深いコピーよりも大幅に高速です。
-
sys.getsizeof()
では差が見えませんが、実際には深いコピーはより多くのメモリを使用しています。これは、Pythonのリスト実装の特性によるものです。
イミュータブルとミュータブル
Pythonのオブジェクトがイミュータブル(変更不可)かミュータブル(変更可能)かによって、コピーの挙動が変わります。
Rev.1: コメント欄の指摘の通り記述に間違いがあり、取り消し線してます。
指摘事項の内容の詳細はコメント欄を参照ください。
# イミュータブルな要素のみのリスト
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
モジュールの動作をカスタマイズできます。
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__
メソッドを適切に実装することで、期待通りの浅いコピーと深いコピーの動作を実現できることがわかります。
循環参照の処理
最後に、循環参照を含む構造でのコピーの挙動を見てみましょう。
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()
が循環参照を適切に処理し、元のオブジェクト構造を維持したコピーを作成できることがわかります。
まとめ
Pythonにおけるリストの浅いコピーと深いコピーについて、以下のポイントを押さえておきましょう:
- 浅いコピーは高速でメモリ効率が良いですが、ネストされたミュータブルオブジェクトの変更に注意が必要です。
- 深いコピーは完全に独立したコピーを作成しますが、パフォーマンスコストが高くなります。
- イミュータブルなオブジェクトに対しては、浅いコピーでも深いコピーでも結果は同じです。
- カスタムクラスでは
__copy__
と__deepcopy__
メソッドをオーバーライドしてコピー動作をカスタマイズできます。 - 循環参照を含む複雑なデータ構造では、
copy.deepcopy()
が適切に処理できます。
リストのコピーは一見単純に見えますが、実際のアプリケーション開発では様々な考慮事項があります。状況に応じて適切なコピー方法を選択し、潜在的な問題を回避することが重要です。この記事で学んだ知識を活かし、より効率的で安全なPythonコードを書いていきましょう。