1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-08-22

はじめに

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

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

image.png

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

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

特徴 浅いコピー (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]

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

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

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

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

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: コメント欄の指摘の通り記述に間違いがあり、取り消し線してます。
指摘事項の内容の詳細はコメント欄を参照ください。

# イミュータブルな要素のみのリスト
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() が循環参照を適切に処理し、元のオブジェクト構造を維持したコピーを作成できることがわかります。

まとめ

image.png

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

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

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

1
2
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?