Pythonの`copy`モジュール
この記事ではPythonのcopyモジュールについて説明します。
浅いコピーと深いコピーの違いを中心に、オブジェクトの複製に関する基本的な仕組みから、カスタムクラスでの応用まで、実際的なサンプルを交えながらわかりやすく解説します。
YouTube Video
Pythonのcopyモジュール
Python の copy モジュールは、オブジェクトの複製(コピー)を扱うための標準モジュールです。浅いコピー(shallow copy)と深いコピー(deep copy)の違いを理解し、カスタムオブジェクトのコピー動作を制御できるようになることが目的です。
基本:浅いコピー(shallow copy)とは何か
ここではリストや辞書などのミュータブルなオブジェクトに対する浅いコピーの挙動を示します。浅いコピーは「最上位のオブジェクトだけを複製し、中の参照は共有する」コピーです。
1# Example: shallow copy with lists and dicts
2import copy
3
4# A nested list
5original = [1, [2, 3], 4]
6shallow = copy.copy(original)
7
8# Mutate nested element
9shallow[1].append(99)
10
11print("original:", original) # nested change visible in original
12print("shallow: ", shallow)このコードでは最上位のリストは複製されますが、内部のサブリストは参照共有されるので、shallow[1].append(99) は original にも反映されます。これは浅いコピーの典型的な振る舞いです。
深いコピー(deep copy)とは何か
深いコピーは、オブジェクトとその内部のすべての参照先を再帰的に複製して「独立した」オブジェクトを生成します。複雑なネスト構造を安全に複製したいときに使います。
1# Example: deep copy with nested structures
2import copy
3
4original = [1, [2, 3], {'a': [4, 5]}]
5deep = copy.deepcopy(original)
6
7# Mutate nested element of the deep copy
8deep[1].append(100)
9deep[2]['a'].append(200)
10
11print("original:", original) # original remains unchanged
12print("deep: ", deep)この例では deep の変更は original に影響しません。deepcopy はオブジェクトグラフ全体をコピーするため、独立したレプリカが得られます。
どちらを使うかの判断基準
どちらのコピーを使うかは、オブジェクトの構造と目的によって判断します。もし内部にあるミュータブル(変更可能な)要素を変更した際に元のオブジェクトへ影響を及ぼしたくない場合は、deepcopy を使用するのが適しています。これは、オブジェクト全体を再帰的に複製し、完全に独立したコピーを作るためです。
一方で、最上位レベルのオブジェクトだけを複製できれば十分であり、速度やメモリ効率を重視したい場合には、copy(浅いコピー)を使う方が適しています。
- 中のミュータブル要素を変更すると元も変わってしまうことを避けたい →
deepcopy。 - 最上位だけ複製できれば十分で、性能(速度・メモリ)を優先したい →
copy(浅いコピー)。
組み込み型やイミュータブルなオブジェクトの扱い
イミュータブル(int, str, tuple のうち中身が不変なものなど)は通常コピー不要です。copy.copy は必要に応じて同一オブジェクトを返します。
1# Example: copying immutables
2import copy
3
4a = 42
5b = copy.copy(a)
6print(a is b) # True for small ints (implementation detail)
7
8s = "hello"
9t = copy.deepcopy(s)
10print(s is t) # True (strings are immutable)イミュータブルオブジェクトはコピーしても効率面で効果がほとんどないため、同一オブジェクトが再利用されることがあります。アプリケーション設計ではこの点を意識する必要はほとんどありません。
カスタムクラス:__copy__ と __deepcopy__ の定義
クラスに対して標準のコピーが期待通りでない場合、自前でコピーロジックを提供できます。__copy__ と __deepcopy__ を定義すると copy.copy / copy.deepcopy がそれらを利用します。
1# Example: customizing copy behavior
2import copy
3
4class Node:
5 def __init__(self, value, children=None):
6 self.value = value
7 self.children = children or []
8
9 def __repr__(self):
10 return f"Node({self.value!r}, children={self.children!r})"
11
12 def __copy__(self):
13 # Shallow copy: create new Node, but reuse children list reference
14 new = self.__class__(self.value, self.children)
15 return new
16
17 def __deepcopy__(self, memo):
18 # Deep copy: copy value and deepcopy each child
19 new_children = [copy.deepcopy(child, memo) for child in self.children]
20 new = self.__class__(copy.deepcopy(self.value, memo), new_children)
21 memo[id(self)] = new
22 return new__copy__および__deepcopy__を実装することで、クラス独自のコピー動作を柔軟に制御できます。たとえば、特定の属性は同じオブジェクトを参照させたい(共有したい)場合や、別の属性は完全に新しいオブジェクトとして複製したい場合などに対応できます。__deepcopy__に渡されるmemo引数は、再帰的なコピーの際にすでに処理済みのオブジェクトを記録しておく辞書で、循環参照による無限ループを防ぐ役割を持ちます。
1# Build tree
2root_node = Node('root', [Node('child1'), Node('child2')])
3
4shallow_copy = copy.copy(root_node) # shallow copy
5deep_copy = copy.deepcopy(root_node) # deep copy
6
7# Modify children
8# affects both root_node and shallow_copy
9shallow_copy.children.append(Node('child_shallow'))
10# affects deep_copy only
11deep_copy.children.append(Node('child_deep'))
12
13# Print results
14print("root_node:", root_node)
15print("shallow_copy:", shallow_copy)
16print("deep_copy:", deep_copy)- このコードでは、変数
shallow_copyは「浅いコピー」によって作成されているため、オブジェクトのトップレベルのみ複製され、内部で参照しているオブジェクト(この場合はchildrenリスト)は元のroot_nodeと共有されます。その結果、shallow_copyに新しいノードを追加すると、共有しているchildrenリストが更新され、root_nodeの内容にも反映されます。 - 一方、変数
deep_copyは「深いコピー」によって作成されているため、内部の構造まで再帰的に複製され、root_nodeとは完全に独立したツリーが生成されます。そのため、deep_copyに新しいノードを追加しても、root_nodeには影響しません。
循環参照(recursive objects)と memo の重要性
複雑なオブジェクトグラフが自分自身を参照している場合(循環参照)に、deepcopy は memo 辞書を使って既にコピーしたオブジェクトを追跡し、無限ループを防ぎます。
1# Example: recursive list and deepcopy memo demonstration
2import copy
3
4a = []
5b = [a]
6a.append(b) # a -> [b], b -> [a] (cycle)
7
8# deepcopy can handle cycles
9deep = copy.deepcopy(a)
10print("deep copy succeeded, length:", len(deep))deepcopyは内部でmemoを使って既に複製済みのオブジェクトを参照するため、循環参照があっても安全にコピーできます。手動で再帰処理する際は同様のメカニズムが必要です。
copyreg とシリアライズ的なカスタムコピー(高度)
ライブラリと協調して特別なコピー動作を登録したい場合、標準ライブラリの copyreg を使ってオブジェクトの (re)construction 方法を登録できます。これは複雑なオブジェクトや C拡張型に対して役立ちます。
1# Example: using copyreg to customize pickling/copy behavior (brief example)
2import copy
3import copyreg
4
5class Wrapper:
6 def __init__(self, data):
7 self.data = data
8
9def reduce_wrapper(obj):
10 # Return callable and args so object can be reconstructed
11 return (Wrapper, (obj.data,))
12
13copyreg.pickle(Wrapper, reduce_wrapper)
14
15w = Wrapper([1, 2, 3])
16w_copy = copy.deepcopy(w)
17print("w_copy.data:", w_copy.data)copyregを使うとpickleとdeepcopyの双方に影響する再構築ルールを提供できます。高度な API なので、通常は__deepcopy__で十分なことが多いです。
実用上の注意点と落とし穴
copy モジュールを使用する際には、正しく動作させるためにいくつかの重要な注意点があります。以下では、実際に開発で遭遇しやすい落とし穴と、その回避のためのポイントを解説します。
- パフォーマンス
deepcopyはメモリと CPU を多く消費する可能性があるため、必要性を吟味しましょう。 - 共有したい要素の扱い
大きなキャッシュなど一部の属性は共有のままにしたい場合、
__deepcopy__内で意図的に参照をコピーしないようにしましょう。 - イミュータブルな内部状態 内部的にイミュータブルなデータを持つならコピーは不要な場合があります。
- スレッドや外部リソース ソケットやファイルハンドルなどコピーできないリソースは、コピーしても意味がないかエラーになるので設計で回避する必要があります。
実用サンプル:辞書の更新を安全に行うパターン
複雑な設定オブジェクトを更新するとき、deepcopy を使って元の設定を保護する例です。
1# Example: safely update a nested configuration using deepcopy
2import copy
3
4default_config = {
5 "db": {"host": "localhost", "ports": [5432]},
6 "features": {"use_cache": True, "cache_sizes": [128, 256]},
7}
8
9# Create a working copy to modify without touching defaults
10working = copy.deepcopy(default_config)
11working["db"]["ports"].append(5433)
12working["features"]["cache_sizes"][0] = 512
13
14print("default_config:", default_config)
15print("working:", working)deepcopy を使うとデフォルト設定を壊す心配なく安全に派生設定を作成できます。特に設定のようなネストされたミュータブル構造では有用です。
ベストプラクティス
copy モジュールを安全かつ効果的に活用するためには、以下のような実践的な指針を押さえておくことが重要です。
- 変更の有無と範囲を考えて、浅いコピーと深いコピーを使い分けます。
- カスタムクラスは
__copy__/__deepcopy__を実装して期待通りの挙動にします。 - 深いコピーはコストが高いため、可能なら設計でコピーの必要を減らします。イミュータブル化、明示的なクローンメソッドなどの利用を検討できます。
- 循環参照を扱う場合は
deepcopyを活用するか、手動でmemo風の仕組みを用意します。 - ファイルハンドルやスレッドなど外部リソースはコピー対象にしないよう設計します。
結論
copy モジュールは、Python におけるオブジェクト複製の標準ツールです。浅いコピーと深いコピーの違いを正しく理解し、必要に応じてカスタムのコピー挙動を実装することで、安全で予測可能な複製操作が可能になります。設計段階で「コピーが本当に必要か」「何を共有したいか」を明確にすると、余計なバグや性能問題を避けられます。
YouTubeチャンネルでは、Visual Studio Codeを用いて上記の記事を見ながら確認できます。 ぜひYouTubeチャンネルもご覧ください。