フリーキーズ | 独学プログラミング

Pythonの浅いコピーと深いコピーを理解しよう!初心者でもわかる違いと使い分け

最終更新日
リンドくん

リンドくん

たなべ先生、Pythonでlist1 = list2としたのに、list1を変更したらlist2も変わってしまいました...なんでですか?

たなべ

たなべ

それは「浅いコピー」と「深いコピー」の違いによるものだね。
Pythonのデータの扱い方にはちょっとした落とし穴があるんだ。今日はそれについてしっかり解説するよ!

コピーの罠

Pythonでプログラミングをしていると、必ず出会う落とし穴の一つが「データのコピー」に関する問題です。
例えば、リストやディクショナリなどのデータを単純に代入しようとすると、思わぬ結果に驚くことがあります。

list1 = [1, 2, 3]
list2 = list1  # コピーのつもり
list2[0] = 100

print(list1)  # [100, 2, 3] と表示される!

「あれ?list1まで変わってしまった...」という経験はありませんか?
これは「浅いコピー」と「深いコピー」という概念を理解していないことが原因です。

今回は、Pythonのデータコピーに関する重要な概念を初心者にもわかりやすく解説します。
この知識は、バグの防止やコードの品質向上に大いに役立ちますので、ぜひ最後まで読んでみてください!

Pythonにおけるオブジェクトと参照の関係

リンドくん

リンドくん

でも先生、「=」で代入したらコピーになるんじゃないんですか?

たなべ

たなべ

実は、Pythonでは「=」はコピーではなく参照の代入なんだよ。
具体例を使って説明するね。

変数とは「名札」のようなもの

Pythonでは、変数はデータそのものではなく、データへの「参照」(または「ポインタ」)だと考えてください。
変数は物につけられた「名札」のようなものです。

例えば、次のコードを見てみましょう。

x = 5
y = x

ここでは、「5」という数値データに「x」という名札をつけています。
そして、y = xでは「xと同じもの」に「y」という別の名札もつけた状態になります。

変更可能(ミュータブル)と変更不可能(イミュータブル)オブジェクト

Pythonのデータ型は、大きく2つに分類できます。

1. イミュータブル(変更不可能)オブジェクト

  • 整数(int)
  • 浮動小数点数(float)
  • 文字列(str)
  • タプル(tuple)
  • 真偽値(bool)

2. ミュータブル(変更可能)オブジェクト

  • リスト(list)
  • 辞書(dict)
  • セット(set)

この区別が、コピーの問題と深く関係しています。
イミュータブルなデータは、値を変更することができないため、代入しても問題が発生しにくいのです。
しかし、ミュータブルなデータは内容を変更できるため、コピーに関する問題が発生しやすくなります。

浅いコピー(Shallow Copy)とは

リンドくん

リンドくん

じゃあ、どうやったら本当のコピーができるんですか?

たなべ

たなべ

まずは「浅いコピー」から説明するね。
これは一歩前進した方法だけど、完全な解決策ではないんだ。

浅いコピーの作り方

Pythonでは以下の方法で浅いコピーを作ることができます。

# 方法1: スライスを使う
list1 = [1, 2, 3]
list2 = list1[:]  # スライス全体を取得

# 方法2: copyメソッドを使う
list1 = [1, 2, 3]
list2 = list1.copy()

# 方法3: list関数を使う
list1 = [1, 2, 3]
list2 = list(list1)

# 方法4: copyモジュールを使う
import copy
list1 = [1, 2, 3]
list2 = copy.copy(list1)

浅いコピーの限界

浅いコピーは、最も外側の構造だけをコピーし、内部の要素はコピーせず参照を共有します。
これは、ネストしたリストや辞書を扱う際に問題を引き起こす可能性があります。

例を見てみましょう。

# ネストしたリストの例
original = [1, 2, [3, 4]]
shallow_copy = original.copy()

# 内部リストを変更
shallow_copy[2][0] = 30

print(original)      # [1, 2, [30, 4]] 内部リストまで変更されてしまう!
print(shallow_copy)  # [1, 2, [30, 4]]

この例では、shallow_copyのネストしたリスト[3, 4]を変更すると、originalの内部リストも変更されてしまいます。
なぜなら、浅いコピーでは内部のリストは参照が共有されているからです。

深いコピー(Deep Copy)とは

リンドくん

リンドくん

ネストしたリストもちゃんとコピーする方法はないんですか?

たなべ

たなべ

もちろんあるよ!それが「深いコピー」というものなんだ。
全ての階層のデータを完全にコピーするんだよ。

深いコピーの作り方

深いコピーを作るには、Pythonのcopyモジュールを使用します。

import copy

# 深いコピーを作成
original = [1, 2, [3, 4]]
deep_copy = copy.deepcopy(original)

# 内部リストを変更
deep_copy[2][0] = 30

print(original)  # [1, 2, [3, 4]] 変更されていない!
print(deep_copy) # [1, 2, [30, 4]]

深いコピーを使うと、元のデータと完全に独立したデータのコピーが作成されるため、一方を変更しても他方には影響しません。

深いコピーのメリットとデメリット

メリット

  • ネストしたデータ構造でも完全に独立したコピーを作成できる
  • 元のデータを変更せずに新しいデータを操作できる

デメリット

  • メモリ使用量が増加する(全データの複製が作成されるため)
  • 大きなデータ構造では処理時間がかかる場合がある

実践的な使い分け方

リンドくん

リンドくん

浅いコピーと深いコピー、どういう場面で使い分ければいいんですか?

たなべ

たなべ

それぞれ使い道があるんだ。具体的な状況に応じた使い分けを説明するね。

浅いコピーを使うべき場面

  1. 単純なリストや辞書の場合 ネストしていない単純なデータ構造なら、浅いコピーで十分です。

    simple_list = [1, 2, 3, 4, 5]
    copy_list = simple_list.copy()  # 浅いコピーで十分
  2. パフォーマンスが重要な場合 大量のデータを扱う場合や処理速度が重要な場合は、浅いコピーの方が効率的です。

  3. 一部の要素だけを変更する場合 元のリストの一部だけを変更したい場合は、浅いコピーが適しています。

深いコピーを使うべき場面

  1. ネストしたデータ構造の場合 リストの中にリストがある場合や、複雑なオブジェクト構造がある場合は深いコピーが必要です。

    complex_data = [1, [2, 3], {'a': [4, 5]}]
    import copy
    safe_copy = copy.deepcopy(complex_data)  # 深いコピーが必要
  2. 元のデータを確実に保護したい場合 元のデータを絶対に変更したくない場合、特にデバッグ時やデータ分析時などは深いコピーを使いましょう。

  3. 副作用を避けたい関数を作る場合 関数内で引数として受け取ったデータを変更すると、呼び出し元のデータも変わる可能性があります。副作用を避けるために深いコピーを使用します。

    def process_data(data):
        # 引数のデータを変更せずに処理するために深いコピーを使用
        import copy
        local_data = copy.deepcopy(data)
        # local_dataを変更しても、元のdataには影響しない
        return processed_result

よくあるバグとその回避方法

リンドくん

リンドくん

実際にこれが原因でバグになることって多いんですか?

たなべ

たなべ

本当によくあるんだよ!特に初心者がハマりやすいバグの一つなんだ。
具体的なケースを見てみよう。

リストを含む関数のデフォルト引数の問題

Pythonで関数のデフォルト引数にミュータブルなオブジェクトを使用すると、予期しない動作が発生することがあります。

def add_item(item, my_list=[]):  # 危険なコード!
    my_list.append(item)
    return my_list

print(add_item("a"))  # ['a']
print(add_item("b"))  # ['a', 'b'] 予想外の結果!

これは、デフォルト引数が関数定義時に一度だけ評価され、その後の関数呼び出しで再利用されるためです。

解決策

def add_item(item, my_list=None):  # 正しい方法
    if my_list is None:
        my_list = []
    my_list.append(item)
    return my_list

print(add_item("a"))  # ['a']
print(add_item("b"))  # ['b'] 予想通りの結果

2次元リスト生成時の問題

初期値として同じリストを複製しようとすると、意図しない結果になることがあります。

# 間違った方法 (3×3の2次元リスト)
grid_wrong = [[0] * 3] * 3
grid_wrong[0][0] = 1
print(grid_wrong)  # [[1, 0, 0], [1, 0, 0], [1, 0, 0]] すべての行が変更される!

解決策

# 正しい方法
grid_correct = [[0] * 3 for _ in range(3)]
grid_correct[0][0] = 1
print(grid_correct)  # [[1, 0, 0], [0, 0, 0], [0, 0, 0]] 期待通りの結果

まとめ

リンドくん

リンドくん

なるほど!データをコピーする時は気をつけないといけないんですね。

たなべ

たなべ

そうだね!Pythonでは変数はラベルのようなものだということを覚えておくのが大切だよ。
必要に応じて浅いコピーと深いコピーを使い分けることで、より安全で予測可能なコードが書けるようになるよ。

Pythonにおける「浅いコピー」と「深いコピー」の違いを理解することは、バグの少ない安定したコードを書くための重要なスキルです。
特にデータの扱いが複雑になるプロジェクトでは、この知識が非常に役立ちます。

ポイントをおさらいしましょう。

  1. 単純な代入(=)はコピーではなく、参照の共有です
  2. 浅いコピーは最も外側のオブジェクトだけをコピーします(list.copy()など)
  3. 深いコピーは内部のオブジェクトも含めて完全にコピーします(copy.deepcopy()
  4. 状況に応じてコピー方法を選ぶことが大切です

この知識を活かして、より信頼性の高いPythonプログラムを書いていきましょう!
困ったときには、「このデータ構造にはどの種類のコピーが適しているか?」と自問してみてください。

関連するコンテンツ