Pythonオブジェクトの浅いコピーとディープコピー

Pythonオブジェクトの浅いコピーとディープコピー

オブジェクトのAssignment statements in Python do not create copies、それらは名前をオブジェクトにバインドするだけです。 不変オブジェクトの場合、通常は違いはありません。

しかし、可変オブジェクトまたは可変オブジェクトのコレクションを操作するには、これらのオブジェクトの「実際のコピー」または「クローン」を作成する方法を探しているかもしれません。

基本的に、withoutを変更できるコピーが必要になる場合があり、同時にオリジナルを自動的に変更します。 この記事では、Python 3でオブジェクトをコピーまたは「クローン」する方法の概要と、いくつかの注意事項を説明します。

Note:このチュートリアルはPython 3を念頭に置いて書かれていますが、オブジェクトのコピーに関してはPython2と3の間にほとんど違いはありません。 違いがある場合は、本文でそれらを指摘します。

Pythonの組み込みコレクションをコピーする方法を見てみましょう。 lists, dicts, and setsのようなPythonの組み込みの可変コレクションは、既存のコレクションでファクトリ関数を呼び出すことでコピーできます。

new_list = list(original_list)
new_dict = dict(original_dict)
new_set = set(original_set)

ただし、このメソッドはカスタムオブジェクトでは機能せず、さらに、shallow copiesのみを作成します。 リスト、dict、セットなどの複合オブジェクトの場合、shallowdeepのコピーには重要な違いがあります。

  • shallow copyは、新しいコレクションオブジェクトを作成し、元のオブジェクトにある子オブジェクトへの参照をそのオブジェクトに入力することを意味します。 本質的に、浅いコピーはone level deepだけです。 コピープロセスは再帰しないため、子オブジェクト自体のコピーは作成されません。

  • deep copyは、コピープロセスを再帰的にします。 最初に新しいコレクションオブジェクトを構築してから、元のコレクションオブジェクトで見つかった子オブジェクトのコピーを再帰的に格納することを意味します。 この方法でオブジェクトをコピーすると、オブジェクトツリー全体をたどって、元のオブジェクトとそのすべての子の完全に独立したクローンを作成します。

ちょっと一口でした。 ディープコピーとシャローコピーの違いを理解するための例をいくつか見てみましょう。

Free Bonus:Click here to get access to a chapter from Python Tricks: The Bookは、Pythonのベストプラクティスと、より美しい+ Pythonicコードをすぐに適用できる簡単な例を示しています。

浅いコピーの作成

以下の例では、新しいネストされたリストを作成し、shallowlylist()ファクトリ関数を使用してそれをコピーします。

これは、ysxsと同じ内容の新しい独立したオブジェクトになることを意味します。 これを確認するには、両方のオブジェクトを調べます。

ysが実際にオリジナルから独立していることを確認するために、少し実験を考えてみましょう。 元のサブリスト(xs)に新しいサブリストを追加してから、この変更がコピー(ys)に影響を与えていないことを確認できます。

>>>

ご覧のとおり、これは期待された効果をもたらしました。 コピーしたリストを「表面的な」レベルで変更しても、まったく問題ありませんでした。

ただし、元のリストのshallowコピーのみを作成したため、ysには、xsに格納されている元の子オブジェクトへの参照が含まれています。

これらの子はnotコピーされました。 それらはコピーされたリストで再び参照されただけです。

したがって、xsの子オブジェクトの1つを変更すると、この変更はysにも反映されます。これはboth lists share the same child objectsが原因です。 コピーは、1レベルの浅いコピーのみです。

>>>

>>> xs[1][0] = 'X'
>>> xs
[[X', 5, 6], [7, 8, 9], ['new sublist']]
>>> ys
[[X', 5, 6], [7, 8, 9]]

上記の例では、(一見)xsにのみ変更を加えました。 しかし、xsandysのインデックス1にあるbothサブリストが変更されたことがわかりました。 繰り返しますが、これは、元のリストのshallowコピーのみを作成したために発生しました。

最初のステップでxsdeepコピーを作成した場合、両方のオブジェクトは完全に独立していました。 これは、オブジェクトの浅いコピーと深いコピーの実際の違いです。

これで、組み込みコレクションクラスの一部の浅いコピーを作成する方法がわかり、浅いコピーと深いコピーの違いがわかりました。 まだ回答が必要な質問は次のとおりです。

  • ビルトインコレクションのディープコピーを作成するにはどうすればよいですか?

  • カスタムクラスを含む任意のオブジェクトのコピー(浅いものと深いもの)を作成するにはどうすればよいですか?

これらの質問に対する答えは、Python標準ライブラリのcopyモジュールにあります。 このモジュールは、任意のPythonオブジェクトの浅いコピーと深いコピーを作成するためのシンプルなインターフェースを提供します。

ディープコピーの作成

前のリストコピーの例を繰り返しますが、1つの重要な違いがあります。 今回は、代わりにcopyモジュールで定義されたdeepcopy()関数を使用してdeepコピーを作成します。

>>>

>>> import copy
>>> xs = [[zs = copy.deepcopy(xs)

copy.deepcopy()で作成したxsとそのクローンzsを調べると、前の例と同じように、両方が再び同じように見えることがわかります。

>>>

ただし、元のオブジェクトの子オブジェクトの1つ(xs)に変更を加えた場合、この変更はディープコピー(zs)には影響しないことがわかります。

今回は、オリジナルとコピーの両方のオブジェクトが完全に独立しています。 xsは、そのすべての子オブジェクトを含めて、再帰的に複製されました。

>>>

Pythonインタープリターに腰を下ろして、今すぐこれらの例を試してみることをお勧めします。 オブジェクトをコピーして頭を包み込むのは、サンプルを実際に体験して遊んでみると簡単です。

ちなみに、copyモジュールの関数を使用して浅いコピーを作成することもできます。 copy.copy()関数は、オブジェクトの浅いコピーを作成します。

これは、コードのどこかに浅いコピーを作成していることを明確に伝える必要がある場合に役立ちます。 copy.copy()を使用すると、この事実を示すことができます。 ただし、ビルトインコレクションの場合、リスト、ディクテーション、およびファクトリ関数のセットを使用して浅いコピーを作成する方が、よりPythonicと見なされます。

任意のPythonオブジェクトのコピー

まだ答えが必要な質問は、カスタムクラスを含む任意のオブジェクトのコピー(浅く深い)をどのように作成するかです。 今それを見てみましょう。

ここでも、copyモジュールが役に立ちます。 そのcopy.copy()およびcopy.deepcopy()関数は、任意のオブジェクトを複製するために使用できます。

繰り返しますが、これらの使用方法を理解する最良の方法は、簡単な実験を行うことです。 これは、前のリストコピーの例に基づいて行います。 簡単な2Dポイントクラスを定義することから始めましょう。

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f'Point({self.x!r}, {self.y!r})'

これが非常に簡単だったことに同意していただければ幸いです。 このクラスから作成されたオブジェクトをPythonインタープリターで簡単に検査できるように、__repr__()実装を追加しました。

上記の例では、Python 3.6 f-stringを使用して、__repr__によって返される文字列を作成しています。 Python 2および3.6より前のPython 3のバージョンでは、たとえば次のような異なる文字列フォーマット式を使用します。

def __repr__(self):
    return 'Point(%r, %r)' % (self.x, self.y)

次に、Pointインスタンスを作成し、copyモジュールを使用して(浅く)コピーします。

>>>

>>> a = Point(23, 42)
>>> b = copy.copy(a)

元のPointオブジェクトとその(浅い)クローンの内容を調べると、期待どおりの結果が得られます。

>>>

>>> a
Point(23, 42)
>>> b
Point(23, 42)
>>> a is b
False

ここで留意すべきことは他にもあります。 ポイントオブジェクトはその座標に不変の型(int)を使用するため、この場合、浅いコピーと深いコピーの間に違いはありません。 しかし、私はすぐに例を拡張します。

さらに複雑な例に移りましょう。 2D長方形を表す別のクラスを定義します。 より複雑なオブジェクト階層を作成できるようにします。私の長方形は、Pointオブジェクトを使用して座標を表します。

class Rectangle:
    def __init__(self, topleft, bottomright):
        self.topleft = topleft
        self.bottomright = bottomright

    def __repr__(self):
        return (f'Rectangle({self.topleft!r}, '
                f'{self.bottomright!r})')

繰り返しますが、まず、四角形インスタンスの浅いコピーを作成しようとします。

rect = Rectangle(Point(0, 1), Point(5, 6))
srect = copy.copy(rect)

元の長方形とそのコピーを調べると、__repr__()オーバーライドがどれほどうまく機能しているか、および浅いコピープロセスが期待どおりに機能したことがわかります。

>>>

>>> rect
Rectangle(Point(0, 1), Point(5, 6))
>>> srect
Rectangle(Point(0, 1), Point(5, 6))
>>> rect is srect
False

前のリストの例がディープコピーとシャローコピーの違いをどのように示していたか覚えていますか? ここでも同じアプローチを使用します。 オブジェクトをオブジェクト階層のより深いところで変更すると、この変更が(浅い)コピーにも反映されます:

>>>

>>> rect.topleft.x = 999
>>> rect
Rectangle(Point(999, 1), Point(5, 6))
>>> srect
Rectangle(Point(999, 1), Point(5, 6))

これがあなたが期待したとおりに動作したことを願っています。 次に、元の四角形のディープコピーを作成します。 その後、別の変更を適用し、影響を受けるオブジェクトを確認します。

>>>

>>> drect = copy.deepcopy(srect)
>>> drect.topleft.x = 222
>>> drect
Rectangle(Point(222, 1), Point(5, 6))
>>> rect
Rectangle(Point(999, 1), Point(5, 6))
>>> srect
Rectangle(Point(999, 1), Point(5, 6))

ほら! 今回は、深いコピー(drect)は、元のコピー(rect)と浅いコピー(srect)から完全に独立しています。

ここでは多くのことを説明しましたが、オブジェクトのコピーにはさらに細かいポイントがあります。

このトピックについて深く掘り下げることは有益なので(ha!)、copy module documentationについて調べたいと思うかもしれません。 たとえば、オブジェクトに特別なメソッド__copy__()__deepcopy__()を定義することで、オブジェクトのコピー方法を制御できます。

覚えておくべき3つのこと

  • オブジェクトの浅いコピーを作成しても、子オブジェクトは複製されません。 したがって、コピーは元のコピーから完全に独立しているわけではありません。

  • オブジェクトのディープコピーは、子オブジェクトを再帰的に複製します。 クローンはオリジナルから完全に独立していますが、ディープコピーの作成は遅くなります。

  • copyモジュールを使用して、任意のオブジェクト(カスタムクラスを含む)をコピーできます。

他の中級レベルのPythonプログラミング手法をさらに詳しく知りたい場合は、次の無料ボーナスをご覧ください。

Free Bonus:Click here to get access to a chapter from Python Tricks: The Bookは、Pythonのベストプラクティスと、より美しい+ Pythonicコードをすぐに適用できる簡単な例を示しています。