Pythonのcopyモジュールの使い方【シャローコピー、ディープコピー】

160, 2021-01-18

目次

Pythonのcopyモジュールの使い方

プログラミング言語であるPython(パイソン)にはcopy(コピー)という標準ライブラリがあります。
このcopyを使うとオブジェクトの「シャローコピー」と「ディープコピー」が可能になります。

シャローコピーは浅いコピー、ディープコピーは深いコピーです。

この記事ではcopyの使い方を解説します。
具体的には↓を見ていきます。

  • シャローコピーとは?

  • ディープコピーとは?

  • copy.copy()の使い方

  • copy.deepcopy()の使い方

  • copy()とdeepcopy()の使い分けについて

シャローコピーとは?

copyではシャローコピーとディープコピーを使い分けることが出来ます。
コピーにおける「シャローコピー」とはどういうものなのでしょうか?

コピーとは一方の値をもう一方の値に複製することをさします。
オブジェクトの場合は、一方のオブジェクトをもう一方に複製することです。

シャローコピーは、オブジェクトのコピーにおいて「浅いコピー」を行います。
「浅いコピー」とは「参照によるコピー」と言い換えることも出来ます。

参照とは、あるオブジェクトを間接的に参照することを指します。
C/C++でいうところのポインタがこれにあたります。

シャローコピーはオブジェクトの属性(メンバ変数)などをコピーするときに、この参照を使ってコピーを行います。
つまり、参照の複製です。参照の複製では実体を直接的にコピーすることはしません。
たとえば属性がリストだったとして、シャローコピーはリスト内のデータの再帰的なコピーは行いません。複製するのはリストの参照の複製だけです。

シャローコピーは参照によるコピーなので、コピー元とコピー先のオブジェクトの属性などは両者間で共有されます。
そのため一方のオブジェクトの属性の値を変更すれば、もう一方のオブジェクトの属性も変化します。

シャローコピーは実体の再帰的なコピーを行わないので、速度的にはディープコピーに勝ります。

ディープコピーとは?

いっぽう「ディープコピー」とはどういうものなのでしょうか。
ディープコピーも一方のオブジェクトをもう一方に複製する動作をします。
シャローコピーが参照によるコピーを行うのに対して、ディープコピーは実体の丸ごとのコピーを行います。

つまりオブジェクトがリストなどの属性を持っていたとして、シャローコピーはそのリストのコピーには参照を使いますが、ディープコピーはリストの要素を再帰的にコピーして複製します。
この再帰的なコピーは参照ではなく実体のコピーなので、オブジェクトの深さによっては動作も重くなるしメモリもたくさん使います。

ディープコピーは実体のコピーを丸ごと行うので、元のオブジェクトと複製されたオブジェクトはまったく別のメモリ上に配置されます。
そのためシャローコピーのように一方のオブジェクトの属性を変更したらもう一方のオブジェクトの属性も変化した、などということは起こりません。

この特性のため、ディープコピーはセキュアな処理などに多用される傾向があります。
つまり、オブジェクトが別のオブジェクトの属性の参照を持っていたら不都合が発生するシーンにおいて、ディープコピーは多用されます。
速度的にはシャローコピーに劣りますが、実体のコピーを行うという点でディープコピーはシャローコピーよりセキュアと言えます。

copy.copy()の使い方

copyモジュールのcopy()関数はオブジェクトのシャローコピーを行う関数です。
構造的には↓のようになっています。

copy.copy(obj)

copy.copy()を使うにはcopyモジュールのインポートが必要です。
copy.copy()は1つの引数を取り、1つの返り値を返します。
引数にはコピー元のオブジェクトを渡します。
返り値にはシャローコピーされたオブジェクトが返ってきます。

copy.copy()は↓のように使うことが出来ます。

import copy


class OwnerList:
    def __init__(self):
        self.owners = ['Bob', 'Taro']


ownerlist = OwnerList()
ownerlist_2 = copy.copy(ownerlist)

OwnerListというクラスを作り、このクラスのオブジェクトをcopy.copy()でシャローコピーしています。
シャローコピーされた結果はownerlist_2に代入されています。

シャローコピーはオブジェクトの属性の再帰的な実体のコピーを行わず、参照によるコピーを行うコピーでした。
オブジェクト自体のidを比較してみます。

ownerlist = OwnerList()
print(id(ownerlist))

ownerlist_2 = copy.copy(ownerlist)
print(id(ownerlist_2))

↑のコードの出力結果は↓のようになります(結果は環境によって変わります)。

29680688
30806704

↑の結果を見ると、オブジェクト自体のidは別物になっています。
ちなみにidの返す値はメモリ上のアドレスを表しています(CPythonの場合)。

問題はオブジェクトの属性のidです。↓のコードで確認します。

ownerlist = OwnerList()
print(id(ownerlist.owners))

ownerlist_2 = copy.copy(ownerlist)
print(id(ownerlist_2.owners))

↑のコードの出力は↓のようになります。

12420840
12420840

↑の結果を見ると、オブジェクトの持つ属性(ownersリスト)のidが共有されていることがわかります。
これがシャローコピーの特徴です。
このようにシャローコピーはオブジェクトの属性を参照でコピーします。
ですので例えば↓のように一方のオブジェクトの属性の値を変更すると、もう一方のオブジェクトの属性にも影響が出ます。

import copy


class OwnerList:
    def __init__(self):
        self.owners = ['Bob', 'Taro']


ownerlist = OwnerList()
ownerlist_2 = copy.copy(ownerlist)  # 参照でコピー

ownerlist.owners.append('Hanako')  # 一方のownersを変更

# 変更が両方のオブジェクトに影響する
print(ownerlist.owners)
print(ownerlist_2.owners)

↑のコードの出力は↓のようになります。

['Bob', 'Taro', 'Hanako']
['Bob', 'Taro', 'Hanako']

↑のコードでは一方のオブジェクトのownersHanakoという文字列を加えています。
その変更が両方のオブジェクトに影響しているのがわかります。

copy.deepcopy()の使い方

copyモジュールのdeepcopy()関数はオブジェクトのディープコピーを行う関数です。
構造的には↓のようになっています。

copy.deepcopy(obj)

copy.deepcopy()を使うにはcopyモジュールのインポートが必要です。
copy.deepcopy()copy.copy()と同様に1つの引数を取り、1つの返り値を返します。
引数にはコピー元のオブジェクトを渡します。
返り値はディープコピーされたオブジェクトです。

copy.deepcopy()は↓のように使うことが出来ます。

import copy


class OwnerList:
    def __init__(self):
        self.owners = ['Bob', 'Taro']


ownerlist = OwnerList()
ownerlist_2 = copy.deepcopy(ownerlist)  # 実体でコピー

deepcopy()もオブジェクト自体のメモリは新しく確保します。
そのためコピー元とコピー先のオブジェクトのidは違います。

ownerlist = OwnerList()
print(id(ownerlist))

ownerlist_2 = copy.deepcopy(ownerlist)  # 参照でコピー
print(id(ownerlist_2))

↑のコードの出力は↓のようになります(環境によって変わります)。

25617456
26744072

問題はオブジェクトの属性ですが、↓のコードでオブジェクトの属性ownersidを確認します。

ownerlist = OwnerList()
ownerlist_2 = copy.deepcopy(ownerlist)  # 参照でコピー

print(id(ownerlist.owners))
print(id(ownerlist_2.owners))

↑のコードの出力は↓のようになります。

20481768
20642632

↑の結果を見るとそれぞれのオブジェクトの持つownersと言う属性のidが違っているのがわかります。
このようにディープコピーはオブジェクトの持つ属性の実体を再帰的にコピーします。
そのため属性はメモリ上では共有されず、それぞれ別のメモリとして配置されます。

copy()とdeepcopy()の使い分けについて

copy()deepcopy()の大きな違いは、メモリ上で共有されるかされないかの違いです。
つまり参照を持っているか持っていないかの違いと言えます。
このためオブジェクトの参照が欲しければcopy()を使い、それ以外はdeepcopy()を使うという使い分けができます。

参照は一方の変更がもう一方に対して影響を与えるという特徴を持っています。
このため、意図しない動作になることが多々あります。
よってオブジェクトに対して何か書き込みの処理を行う場合は、この参照の振る舞いについて考慮しておく必要があります。
逆に読み込みの処理だけを行う場合は参照で十分と言えます。なぜなら値を変更しないからです。
(もちろん読み取り中に値が変更されたら困る場合はディープコピーの方が適しています)

deepcopy()は実体のコピーを行うので、動作は少し遅くなりますが、その分セキュアなコピーが可能です。
コピーされたオブジェクトを変更しても、その変更が他のオブジェクトに波及するということがありません。

この2つのコピーの特性を把握して使い分けることでプログラムの品質を上げることが出来ると思います。

おわりに

今回はcopyモジュールの使い方を解説しました。
コピーはプログラミングで最も使う機能と言ってもいいですが、けっこう奥が深いものでした。

コピーの道は長い