Skip to content

9.7 ミュータブルなオブジェクトをコピーする — copy

Pythonでリストや辞書などの「ミュータブル(変更可能)なオブジェクト」を扱う際、初心者が必ずと言っていいほど直面するバグがあります。それは、「コピーしたつもりのデータを変更したら、元のデータまで書き換わってしまった」という現象です。

これはPythonの変数代入が「値のコピーではなく、参照(メモリ上の場所)の共有」を行っているために発生します。

本記事では、「9.7 ミュータブルなオブジェクトをコピーする」として、この罠を回避し、データを安全に複製するための copy モジュールの使い方を解説します。

1. 罠の正体:単純な代入( = )はコピーではない

Section titled “1. 罠の正体:単純な代入( = )はコピーではない”

まずは、なぜ copy モジュールが必要なのかを確認しましょう。

# 元のリスト
original_list = [1, 2, 3]
# 単純な代入でコピーした「つもり」
copied_list = original_list
# コピー先を変更してみる
copied_list[0] = 99
# 結果の確認
print("copied:", copied_list) # [99, 2, 3]
print("original:", original_list) # [99, 2, 3] <- 元のリストまで変わっている!

copied_list = original_list という処理は、データそのものを複製しているわけではありません。「同じデータが入っている箱に、もう一つ別の名前(タグ)を付けただけ」です。そのため、どちらの名前から変更を加えても、中身のデータは共通して変更されてしまいます。

2. 浅いコピー(Shallow Copy):copy.copy()

Section titled “2. 浅いコピー(Shallow Copy):copy.copy()”

一番外側のリスト(コンテナ)だけを新しく作り直したい場合は、copy モジュールの copy() 関数を使用します。これを「浅いコピー」と呼びます。 (※ リスト型の場合は list.copy() メソッドや、スライス [:] を使っても浅いコピーになります)

import copy
original_list = [1, 2, 3]
# 浅いコピーを実行
shallow_copied = copy.copy(original_list)
# コピー先を変更
shallow_copied[0] = 99
print("copied:", shallow_copied) # [99, 2, 3]
print("original:", original_list) # [1, 2, 3] <- 元のリストは安全!

これで解決したように見えますが、実は「浅いコピー」には弱点があります。リストの中にさらにリストが含まれている(ネストされている)場合、内側のリストは依然として「参照が共有されたまま」になるのです。

import copy
# リストの中にリストが入っている(ネスト構造)
original_nested = [1, 2, ['A', 'B']]
shallow_copied = copy.copy(original_nested)
# 一番外側の要素を変更 -> 元データに影響なし
shallow_copied[0] = 99
# 内側のリストの要素を変更 -> 元データも変わってしまう!
shallow_copied[2][0] = 'Z'
print("copied:", shallow_copied) # [99, 2, ['Z', 'B']]
print("original:", original_nested) # [1, 2, ['Z', 'B']] <- 内側が変わってしまった!

3. 深いコピー(Deep Copy):copy.deepcopy()

Section titled “3. 深いコピー(Deep Copy):copy.deepcopy()”

ネストされた複雑なデータ構造(リストの中のリストや、辞書の中の辞書など)を、すべて完全に独立した別のデータとして複製したい場合は、deepcopy() を使用します。これを「深いコピー」と呼びます。

import copy
original_nested = [1, 2, ['A', 'B']]
# 深いコピーを実行
deep_copied = copy.deepcopy(original_nested)
# 内側のリストの要素を変更
deep_copied[2][0] = 'Z'
print("copied:", deep_copied) # [1, 2, ['Z', 'B']]
print("original:", original_nested) # [1, 2, ['A', 'B']] <- 完全に安全!

deepcopy() は、データ構造を再帰的に(一番深いところまで)たどり、すべて新しいオブジェクトとしてメモリ上に作り直してくれます。

4. まとめ:どのように使い分けるべきか?

Section titled “4. まとめ:どのように使い分けるべきか?”
  • 単純な代入 (=): 単純に変数名に別名(エイリアス)を付けたい場合のみ。
  • 浅いコピー (copy()): 1次元のリストや辞書(中にリストなどを含まない)を複製したい場合。処理が速く、メモリ消費も少ない。
  • 深いコピー (deepcopy()): JSONデータのような、何重にもネストされた複雑なデータ構造を完全に独立して複製したい場合。処理は少し重くなりますが、予期せぬバグを完全に防げます。

データの構造(ネストの有無)を把握し、適切なコピー手法を選択することが、バグのない堅牢なコードを書く秘訣です。