copyモジュール 浅いコピーと深いコピー

標準ライブラリのcopyモジュールを使用するとオブジェクトを完全にコピーすることができます。ある程度プログラミングに馴染みがある方には説明不要かもしれませんが、最初に浅いコピーと深いコピーについても解説します。

変数のコピー

「変数をコピーする」という処理はプログラムをする上でよく行われる操作ですが、一言にコピーといってもいくつかの種類があります。以下、順にコピーの段階について説明を進めます。

コピーその1 単純な代入

まず、dict型オブジェクトを生成し、別の変数に代入する場合を考えてみましょう。

# その1 単純な代入
data1 = {'key1' : 100}
data2 = data1   # 変数をコピー?

del data2['key1'] # data2の値を削除する

# 同じオブジェクトを参照しているので、オリジナルの方も削除されている
print(data1) # {}
print(data2) # {}

上のコードでは、変数data2はdata1を代入して定義されています。data1とdata2は同じオブジェクトなので、どちらかに変更を加えるともう一方にも変更が反映されます。つまり、参照がコピーされているだけなのでオブジェクト自身がコピーされているわけではないのです。

イメージ的には以下の図のようになります。

コピーその2 浅いコピー

では次に、オブジェクト自体をコピーしてみましょう。Pythonでオブジェクトをコピーする場合、標準ライブラリのcopyモジュールを使用します。copyモジュールには浅いコピーと深いコピーが用意されています。まずは浅いコピーから見てみましょう。

import copy

# その2 浅いコピー
data1 = {'key1' : 100, 'key2': [1, 2]}
data2 = copy.copy(data1)

del data2['key1'] # data2の値を削除する

# オリジナルの方は削除されていない
print(data1)  # {'key1': 100, 'key2': [1, 2]}
print(data2) # {'key2': [1, 2]}

# ところが、オブジェクトへの参照は同じなので・・・
data2['key2'][0] = 999
print(data1)  # {'key1': 100, 'key2': [999, 2]}
print(data2) # {'key2': [999, 2]}

2019/9/6一部修正 変数名等にタイポがありました。(ご連絡くださった方ありがとうございます。)

上のコードでは、data1が参照するオブジェクトをコピーしてdata2(が参照するオブジェクト)を作成しています。5行目でdata2に対して操作を行っていますが、data1の方には影響が出ていません。ですが、オブジェクト内部で保持しているオブジェクトへの参照は同じなので、そこに対する操作を行うと相互に影響が出てしまいます。11行目でdata2のキーに"key2"を指定してオブジェクトを取り出し操作を行うと、data1["key2"]にも影響が出ていることが確認できます。

イメージ的には以下のようになります。

コピーその3 深いコピー

深いコピー

では、コピーしたオブジェクトの参照先のオブジェクトもコピーする場合はどうすればよいでしょうか?それが深いコピーです。deepcopyメソッドを使用します。

import copy

# その3 深いコピー
data1 = {'key1' : 100, 'key2': [1, 2]}
data2 = copy.deepcopy(data1)

# オリジナルと参照先が異なるので更新されていない 
data2['key2'][0] = 999
print(data1)  # {'key1': 100, 'key2': [1, 2]}
print(data2) # {'key1': 100, 'key2': [999, 2]}

イメージ的には以下のようになります。

独自クラスの深いコピー

上のサンプルでは組込みのdict型、list型に対して深いコピーをしましたが、独自クラスが参照される場合、__deepcopy__という特殊メソッドを実装する必要があります。サンプルで見てみましょう。

import copy

class Sample():

    def __init__(self, text):
        self.text = text 

    def __repr__(self):
        return self.text

    def __deepcopy__(self, memo):
        """ 自分自身と同じオブジェクトを生成し、返す """
        new_obj = Sample(self.text)
        return new_obj


# その3 深いコピー
data1 = {'key1' : 100, 'key2': Sample('obj')}
data2 = copy.deepcopy(data1)

# コピーされていることを確認する
data2['key2'].text = 'hoge' 
print(data1)
print(data2)

Sample型のオブジェクトを参照する辞書をコピーしていますが、Sample型のオブジェクトもコピーされ、data1とdata2それぞれが独立したデータを持っていることが確認できます。ですが、__deepcopy__メソッドを実装しない場合は、(コード自体は動きますが)23行目と24行目の出力が同じになります。これは、Sample型のオブジェクトは浅いコピーとなっていることが原因です。