Python のコピー, copy ってなに?







Python のコピーには2種類あります。1つは「浅いコピー」。もう1つは「深いコピー」です。 「浅いコピー」も「深いコピー」も他のプログラミング言語でも使われる言葉なので 覚えておいてそんはないかなと思います。

では「浅いコピー」も「深いコピー」とは、どういう意味でしょうか?





浅いコピーちょっとだけコピー
深いコピー全部コピー













f:id:domodomodomo:20171106122644j:plain




何を言っているのか、全くわかりません。そこでスライドを作りました。まず identity とは何かについて、簡単にイメージ説明して、そのあと copy と deepcopy の違いを説明しています。





しかし、なぜ2種類あるのでしょうか? それは Python のオブジェクトの構造と関係があります。

オブジェクトは属性を持っています。そしてその属性には別のオブジェクトが代入されています。 その別のオブジェクトは属性を持っていて、さらにその属性には別の別のオブジェクトが代入されています。

一体、どこまでコピーすればいいのでしょうか? それはきっとその時と場合によると思いますが、提供されている機能は2つに大別されます。

変数に代入された名前空間だけ生成するコピーを「浅いコピー」と呼びます。 変数とその配下の属性全ての名前空間を生成するコピーを「深いコピー」と呼びます。

しかし「深いコピー」の全てとはなんでしょうか? 全てのオブジェクトは属性を持ちます。コピーが止まることがありません。

Python の「深いコピー」はイミュータブルな名前空間は、コピーせず、そこで動作を止めてしまいます。 ちなみにリスト list, 集合 set, 辞書 dict は copy メソッドを持っていますが、このコピーは浅いコピーです。


実際に触って動作を確認してみる。

オブジェクトが持っている identity を全て表示する ids という関数を作って、実際にどのようにコピーされているかを確認して見ました。identity がどのように変化するでしょうか。

def sample_code():

    pc = Computer(
        Cpu('2.3GHz', 5),
        Memory('8GB', '2133MHz', 'DDR4'),
        Ssd('256GB'))

    print('# pc')
    pprint.pprint(ids(pc))

    print('# copy.copy(pc)')
    print('#   copy.copy creates only one instance\n'
          '#   contained in variable.')
    pprint.pprint(ids(copy.copy(pc)))

    print('# copy.deepcopy(pc)')
    print('#   copy.deepcopy creates all intances\n'
          '#   contained in variables and attributes except immutable objects')
    pprint.pprint(ids(copy.deepcopy(pc)))
$ python ids.py 
# pc
(4510800024,
 {'auxiliary_memory': (4510185176, {'volume': 4510149464}),
  'cpu': (4510150328, {'clock': 4509278648, 'core': 4507450848}),
  'primary_memory': (4510150384,
                     {'clock': 4510076408,
                      'type_': 4510076464,
                      'volume': 4510076352})})
# copy.copy(pc)
#   copy.copy creates only one instance
#   contained in variable.
(4510800080,
 {'auxiliary_memory': (4510185176, {'volume': 4510149464}),
  'cpu': (4510150328, {'clock': 4509278648, 'core': 4507450848}),
  'primary_memory': (4510150384,
                     {'clock': 4510076408,
                      'type_': 4510076464,
                      'volume': 4510076352})})
# copy.deepcopy(pc)
#   copy.deepcopy creates all intances
#   contained in variables and attributes except immutable objects
(4510800080,
 {'auxiliary_memory': (4511027720, {'volume': 4510149464}),
  'cpu': (4510820392, {'clock': 4509278648, 'core': 4507450848}),
  'primary_memory': (4510872856,
                     {'clock': 4510076408,
                      'type_': 4510076464,
                      'volume': 4510076352})})
$


copy, deepcopy 関数は、str や int などの immutable なオブジェクトは、singleton として取り扱い、インスタンス化しません。

def _copy_immutable(x):
    return x


まったく関係ないですが CPython では int は - 5 以上 256 以下の値を singleton として扱っている気配があります。例えば

- 5 以上 256 以下では a == b なら a is b となります。
それ以外では a == b であっても a is not b になります。

>>> # - 5 以上 256 以下以外の整数のリストが返されます。
>>> [a for a, b in zip(range(-10, 262), range(-10, 262)) if a is not b]
[-10, -9, -8, -7, -6, 257, 258, 259, 260, 261]
>>>


例えば、 a = 1; b = 1 とすると、 a と b は値 1 を持つ同じオブジェクトを参照するときもあるし、 そうでないときもあります。これは "実装に依存" します。
3.1. オブジェクト、値、および型


copy メソッドと copy 関数

さて2つの違いは何でしょうか?

import copy
copy(list(range(3)))
list(range(3)).copy()


関数は、メソッドを呼び出しているだけです。

# Step 1
copy(list(range(3)))
# Step 2
list(range(3)).copy()


list, dict, set には専用の copy メソッドがあります。 list, dict, set オブジェクトを関数で copy するときは、 単純にそのメソッドを呼び出しています。

d[list] = list.copy
d[dict] = dict.copy
d[set] = set.copy
d[bytearray] = bytearray.copy


このようにして copy という処理については、関数で呼び出す方法とメソッドで呼び出す方法が、混在してしまっています。 これが公式サイトの FAQ に記載されていた "粗探しのしようが" あるところかなと思ったりもします。

個々のケースについては粗探しのしようがありますが、Python の一部であるし、根本的な変更をするには遅すぎます。
Python にメソッドを使う機能 (list.index() 等) と関数を使う機能 (len(list) 等) があるのはなぜですか?


これを解消するには各クラスで __copy__ メソッドを実装する必要があるかなと思います。確かに "根本的な変更をする" のは、結構難しそうですね。