Pythonの`copy`モジュール

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 の重要性

複雑なオブジェクトグラフが自分自身を参照している場合(循環参照)に、deepcopymemo 辞書を使って既にコピーしたオブジェクトを追跡し、無限ループを防ぎます。

 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 を使うと pickledeepcopy の双方に影響する再構築ルールを提供できます。高度な 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チャンネルもご覧ください。

YouTube Video