Python 3: Deep Dive (Part 2 - Iterators, Generators): 深いコピー (セクション2-3/14)
Python でのシーケンスのコピーには浅いコピーと深いコピーがあり、その違いを理解することが重要です。
浅いコピーは新しいオブジェクトを作成しますが、内部の要素は元のオブジェクトと同じ参照を共有します。
深いコピーはすべてのネストされたオブジェクトを再帰的にコピーし、完全に独立した新しいオブジェクトを作成します。
Python でシーケンスをコピーすることは、一見簡単に見えるかもしれませんが、表面的には見えない多くのことが起こっています。リスト、タプル、またはカスタムオブジェクトを扱う際、コピーの仕組みを理解することは、プログラムで予期せぬ動作を防ぐために非常に重要です。この記事では、シーケンスのコピーの複雑さに深く踏み込み、浅いコピーと深いコピーの両方を探り、これらの概念を説明するための実践的な例を提供します。
なぜシーケンスをコピーするのか?
「どのように」に入る前に、「なぜ」について議論しましょう。Python では、リストのような可変シーケンスは作成後に変更できます。この可変性は、特に複数の参照が同じオブジェクトを指している場合、意図しない副作用を引き起こす可能性があります。シーケンスをコピーすることで、独立したオブジェクトを作成し、一方の変更が他方に影響を与えないようにすることができます。
例:
original_list = [1, 2, 3]
copied_list = original_list # コピーではなく、単なる別の参照
copied_list.append(4)
print("元のリスト:", original_list)
print("コピーされたリスト:", copied_list)
出力:
元のリスト: [1, 2, 3, 4]
コピーされたリスト: [1, 2, 3, 4]
ご覧の通り、`copied_list` を変更すると `original_list` も影響を受けます。これは、両方の変数がメモリ内の同じリストを指しているためです。
Python でシーケンスをコピーする方法
Python でシーケンスをコピーするには、それぞれに独自の使用例と微妙な違いがある複数の方法があります。
1. 単純なループ
ループを使用して手動で要素をコピーできます。
original = [1, 2, 3]
copied = []
for item in original:
copied.append(item)
print(copied)
出力:
[1, 2, 3]
この方法は新しいリストを作成し、各要素を個別にコピーします。しかし、これはあまり Python らしくなく、大きなシーケンスに対しては非効率的かもしれません。
2. リスト内包表記
より Python らしい方法は、リスト内包表記を使用することです。
original = [1, 2, 3]
copied = [item for item in original]
print(copied)
出力:
[1, 2, 3]
リスト内包表記は簡潔で、通常、手動のループよりも高速です。
3. `copy()` メソッドの使用
リストのような可変シーケンスには、組み込みの `copy()` メソッドがあります。
original = [1, 2, 3]
copied = original.copy()
print(copied)
出力:
[1, 2, 3]
タプルや文字列のような不変シーケンスには `copy()` メソッドがないことに注意してください。
4. スライシングの使用
スライシングを使用してシーケンスの浅いコピーを作成できます。
original = [1, 2, 3]
copied = original[:]
print(copied)
出力:
[1, 2, 3]
リストの場合、これは同じ要素への参照を持つ新しいリストを作成します。ただし、タプルや文字列でこの方法を使用する際は注意が必要です。後ほど詳しく説明します。
5. `list()` 関数の使用
`list()` 関数を使用して、任意のイテラブルから新しいリストを作成できます。
original = [1, 2, 3]
copied = list(original)
print(copied)
出力:
[1, 2, 3]
この方法は、リストのコピーや他のイテラブルをリストに変換するのに適しています。
6. `copy` モジュールの使用
Python の `copy` モジュールは、汎用的な `copy()` 関数を提供します。
import copy
original = [1, 2, 3]
copied = copy.copy(original)
print(copied)
出力:
[1, 2, 3]
この関数は、カスタムオブジェクトを扱う場合や、明示的に浅いコピーを作成していることを示したい場合に便利です。
不変型に注意
タプルや文字列のような不変型を扱う場合、コピーの動作は同じではありません。
タプルのコピー
original = (1, 2, 3)
copied = tuple(original)
print("コピー:", copied)
print("同じオブジェクト:", original is copied)
出力:
コピー: (1, 2, 3)
同じオブジェクト: True
`tuple()` を使ってタプルを「コピー」したにもかかわらず、`copied` は実際には `original` と同じオブジェクトです。タプルは不変なので、Python はメモリ使用を最適化するために新しいオブジェクトを作成しません。
タプルのスライシング
original = (1, 2, 3)
copied = original[:]
print("コピー:", copied)
print("同じオブジェクト:", original is copied)
出力:
コピー: (1, 2, 3)
同じオブジェクト: True
ここでも、タプル全体をスライスしても新しいオブジェクトは作成されません。
文字列のコピー
同じ概念が文字列にも適用されます。
original = "Python"
copied = original[:]
print("コピー:", copied)
print("同じオブジェクト:", original is copied)
出力:
コピー: Python
同じオブジェクト: True
不変オブジェクトは変更できないため、コピーを作成する必要はなく、Python は元のオブジェクトを返します。
浅いコピーと深いコピー
可変オブジェクトを含むシーケンスを扱う際、浅いコピーと深いコピーの違いを理解することが重要です。
浅いコピー
浅いコピーは新しいオブジェクトを作成しますが、元のオブジェクトに見つかった項目への参照をそこに挿入します。言い換えれば、外部構造はコピーされますが、内部要素は同じままです。
例:
original = [[1, 2], [3, 4]]
copied = original.copy()
copied[0][0] = 'X'
print("元のリスト:", original)
print("コピーされたリスト:", copied)
出力:
元のリスト: [['X', 2], [3, 4]]
コピーされたリスト: [['X', 2], [3, 4]]
`original` のコピーを作成したにもかかわらず、`copied` のネストされた要素を変更すると `original` にも影響を与えます。これは、両方のリストが同じ内部リストへの参照を含んでいるためです。
なぜこれが起こるのか?
リストをコピーする(または任意の浅いコピー方法を使用する)とき、Python は新しいリストオブジェクトを作成しますが、その中の要素は元の要素への参照のままです。
original_list --> [ [1, 2] , [3, 4] ]
^ ^
| |
copied_list ----|-------------|
`original_list` と `copied_list` の両方が同じ内部リストを指しています。
深いコピー
深いコピーは新しいオブジェクトを作成し、元のオブジェクトに見つかったネストされたオブジェクトのコピーを再帰的に追加します。
`copy.deepcopy()` の使用:
import copy
original = [[1, 2], [3, 4]]
copied = copy.deepcopy(original)
copied[0][0] = 'X'
print("元のリスト:", original)
print("コピーされたリスト:", copied)
出力:
元のリスト: [[1, 2], [3, 4]]
コピーされたリスト: [['X', 2], [3, 4]]
今度は、`copied` を変更しても `original` に影響を与えません。これは、すべてのネストされたオブジェクトが再帰的にコピーされたためです。
`deepcopy()` はどのように機能するのか?
`copy` モジュールの `deepcopy()` 関数は、循環参照やカスタムオブジェクトのような複雑なケースを処理しながら、すべてのネストされたオブジェクトの徹底的なコピーを行います。
カスタムオブジェクトと深いコピー
カスタムクラスを扱う場合、オブジェクトのコピー方法を制御するために `copy()` および `deepcopy()` メソッドを実装する必要があるかもしれません。
例:
import copy
class MyClass:
def __init__(self, value):
self.value = value
obj = MyClass([1, 2, 3])
shallow_copied_obj = copy.copy(obj)
deep_copied_obj = copy.deepcopy(obj)
# 元のオブジェクトのリストを変更
obj.value.append(4)
print("元のオブジェクト:", obj.value)
print("浅くコピーされたオブジェクト:", shallow_copied_obj.value)
print("深くコピーされたオブジェクト:", deep_copied_obj.value)
出力:
元のオブジェクト: [1, 2, 3, 4]
浅くコピーされたオブジェクト: [1, 2, 3, 4]
深くコピーされたオブジェクト: [1, 2, 3]
この場合、浅いコピーは元のオブジェクトと同じリスト参照を共有していますが、深いコピーはリストの独自の別のコピーを持っています。
ベストプラクティス
浅いコピーを使用する場合:
シーケンスに不変要素のみが含まれている場合。
ネストされた可変要素を変更する意図がない場合。
パフォーマンスが重要で、深いコピーのオーバーヘッドを避けたい場合。
深いコピーを使用する場合:
シーケンスに独立して変更する必要があるネストされた可変要素が含まれている場合。
複数のネストレベルを持つ複雑なオブジェクトを扱っている場合。
不変型に注意:
不変型をコピーしても新しいオブジェクトは作成されないことを覚えておいてください。
可変要素を含むタプルや文字列をコピーする際は注意が必要です。
カスタムクラス:
クラスが特別なコピー動作を必要とする場合は、`copy()` と `deepcopy()` を実装してください。
結論
Python でのシーケンスのコピーは、最初に思われるよりも微妙です。浅いコピーと深いコピーの違いを理解することは、堅牢でエラーのないコードを書くために不可欠です。常にシーケンス内の要素の型を考慮し、ニーズに合った適切なコピー方法を選択してください。
これらの概念をマスターすることで、可変シーケンスをより適切に扱い、プログラムで意図しない副作用を防ぐことができるでしょう。
参考文献:
Python ドキュメントの Copy モジュール
Luciano Ramalho 著「Fluent Python」