Last Updated: 2/6/2024, 5:44:57 AM

# コピーってなに?







Python のコピーには2種類あります。 1つは「浅いコピー」。もう1つは「深いコピー」です。

標準ライブラリ copy (opens new window) には 浅いコピーをする copy 関数と 深いコピーをする deepcopy 関数が用意されています。

そしてこれまた公式ドキュメントの説明が鬼で、 たった一行で説明されています。

copy.copy(x)
x の浅い (shallow) コピーを返します。

copy.deepcopy(x[, memo])
x の深い (deep) コピーを返します。

copy - Python 標準ライブラリ (opens new window)

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



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
















この2つのコピーにどのような違いがあるのでしょうか。 「浅いコピー」も「深いコピー」も、 他のプログラミング言語でも使われる言葉なので 覚えておいてそんはないかなと思います。

そして地味に問題なのが、 このコピーというのが、言葉の割に地味に難しく、 理解に時間を要するということです。 理解してしまえば、大したことはないのですが...

Hello, world!

# 1. 触ってみる

# 1.1. 浅いコピー

list や dict に最初からメソッドとしてついてくる copy メソッドは、 いずれも浅いコピーです。

[0, 1, 2].copy()
{'a':0, 'b':1, 'c':2}.copy()
>>> [0, 1, 2].copy()
[0, 1, 2]
>>>
>>> {'a':0, 'b':1, 'c':2}.copy()
{'a': 0, 'b': 1, 'c': 2}
>>>
問題

実行結果 1 には何が表示されますか?

>>> print(lst0)
[0, 1, 2]
>>> print(lst1)
[0, 1, 2]
>>> print(lst2)  # <--- ここには何が表示されますか?
実行結果 1
>>>

実行したコードは以下の通りです。

from copy import copy

#
# 1) 3つのリストを作る。
#
lst0 = [None for _ in range (3)]
lst1 = lst0
lst2 = copy(lst0)  # lst0.copy() と完全に同じことをしています。

#
# 2) lst0 を変更する。
#
lst0[0], lst0[1], lst0[2] = 0, 1, 2

#
# 3) 結果を表示する。
#
print(lst0)
print(lst1)
print(lst2)  # <--- ここには何が表示されますか?
# 実行結果1

実行結果 1 に最も近いものを次の選択肢から選んでください。

では次に、二次元リストを見ていきたいと思います。

問題

実行結果2には何が表示されますか?

>>> print(lst0[0])
[0, 1, 2]
>>> print(lst1[0])
[0, 1, 2]
>>> print(lst2[0])  # <--- ここには何が表示されますか?
実行結果 2
>>>

実行したコードは以下の通りです。

from copy import copy

#
# 1) リストを作る。
#
lst0 = [[None for _ in range (3)] for _ in range(3)]
# lst0 = [None for _ in range (3)]
lst1 = lst0
lst2 = copy(lst0)

#
# 2) 変数 lst0 を変更する。
#
lst0[0][0], lst0[0][1], lst0[0][2] = 0, 1, 2
# lst0[0], lst0[1], lst0[2] = 0, 1, 2

#
# 3) 結果を表示する。
#
print(lst0[0])
print(lst1[0])
print(lst2[0])  # 実行結果2 <--- ここには何が表示されますか?
# print(lst0)
# print(lst1)
# print(lst2)

実行結果2に最も近いものは次のうち、どれですか?

# 1.2. 深いコピー

問題

実行結果3には何が表示されますか?

>>> print(lst0[0])
[0, 1, 2]
>>> print(lst1[0])
[0, 1, 2]
>>> print(lst2[0])
実行結果3
>>> 

実行したコードは以下の通りです。

from copy import deepcopy

#
# 1) リストを作る。
#
lst0 = [[None for _ in range (3)] for _ in range(3)]
lst1 = lst0
lst2 = deepcopy(lst0)

#
# 2) 変数 lst0 を変更する。
#
lst0[0][0], lst0[0][1], lst0[0][2] = 0, 1, 2
# lst0[0], lst0[1], lst0[2] = 0, 1, 2

#
# 3) 結果を表示する。
#
print(lst0[0])  # 実行結果3 <--- ここには何が表示されますか?
print(lst1[0])
print(lst2[0])

実行結果3に最も近いものは次のうち、どれですか?





# 2. 調べてみる

# 2.1. 全体像

スライドを作りました。 まず identity とは何かについて、簡単にイメージを見て、 そのあと copydeepcopy の違いを説明しています。

リンク先のスライドにまとめました。




# 2.2. コードで実験してみる

オブジェクトが持っている identity を全て表示する ids という関数を作りました。

この ids 関数を使い、 copy, deepcopy されたオブジェクトの属性に束縛された identity が どのように変化するかを確認します。

以下の Gist のコードを、そのまま対話モード >>> にコピペして貼り付けて見てください。

Computer, Cpu, Memory, Ssd クラスは、 今回のためにだけ作ったサンプルクラスです。

json.dumps は標準ライブラリ json (opens new window) の関数で 辞書 dict を print 関数で表示しやすい文字列 str にして返してくれます。

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

# original
print(json.dumps(ids(computer), indent=4))

# copy
print(json.dumps(ids(copy.copy(computer)), indent=4))

# deepcopy
print(json.dumps(ids(copy.deepcopy(computer)), indent=4))

# (1) オリジナルのオブジェクト

これがコピー元のオブジェクトの identity です。

>>> # original
... print(json.dumps(ids(computer), indent=4))
[
    4424006792,
    {
        "cpu": [
            4424006624,
            {
                "clock": 4424006320,
                "core": 4420247872
            }
        ],
        "primary_memory": [
            4424006680,
            {
                "volume": 4424006376,
                "clock": 4424006432,
                "type_": 4424006488
            }
        ],
        "auxiliary_memory": [
            4424006736,
            {
                "volume": 4424006544
            }
        ]
    }
]
>>>

# (2) 浅いコピーしたオブジェクト

浅いコピーをしたオブジェクトの identity です。 直接、変数に束縛されたオブジェクトの identity 4424006792 と 浅いコピーされたオブジェクトの dientity 4424007072 だけ異なります。 それ以外は、全て同じ identity です。

>>> # copy
... print(json.dumps(ids(copy.copy(computer)), indent=4))
[
    4424007072,  <----------- ここだけ変化しました。
    {
        "cpu": [
            4424006624,
            {
                "clock": 4424006320,
                "core": 4420247872
            }
        ],
        "primary_memory": [
            4424006680,
            {
                "volume": 4424006376,
                "clock": 4424006432,
                "type_": 4424006488
            }
        ],
        "auxiliary_memory": [
            4424006736,
            {
                "volume": 4424006544
            }
        ]
    }
]
>>>

# (3) 深いコピーをしたオブジェクト

一つ下の階層 auxiliary_memory, cpu, primary_memory に束縛されていた identity まで変化しました。

しかし、一つ下の階層よりさらに下の階層の identity は変化しませんでした。 deepcopy は int, str などの変更できないオブジェクトは新規にオブジェクトを生成しません。

>>> # deecomputeropy
... print(json.dumps(ids(copy.deepcopy(computer)), indent=4))
[
    4424007520,           <-------- 変化しました。
    {
        "cpu": [
            4424007576,   <-------- 変化しました。
            {
                "clock": 4424006320,
                "core": 4420247872
            }
        ],
        "primary_memory": [
            4424007016,   <-------- 変化しました。
            {
                "volume": 4424006376,
                "clock": 4424006432,
                "type_": 4424006488
            }
        ],
        "auxiliary_memory": [
            4424007296,   <-------- 変化しました。
            {
                "volume": 4424006544
            }
        ]
    }
]
>>>
(後編)ここからは重箱の隅をつつく重要でない話をします。

# 3. 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) 等) があるのはなぜですか? (opens new window)

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

# 4. 同じオブジェクトを返すもの
(どうでもいいこと)

# 4.1. 浅いコピー copy の場合

copy は str, int, tuple などの immutable なオブジェクトは、 同じオブジェクトをそのまま返します。

import copy
t1 = ([], )
t2 = copy.copy(t1)
t1 is t2
>>> t1 is t2
True
>>>

# 4.2. 深いコピー deepcopy の場合

deepcopy 関数は「完全に変更できない」オブジェクトは、同じオブジェクトをそのまま返して、 属性を再帰的にコピーすることを辞めます。

「完全に変更できない」オブジェクトとは immutable のことではありません。 例えば tuple の中に list がはいったオブジェクトは変更できるので、 新しくインスタンスが生成されます。

import copy
t1 = ([], )
t2 = copy.deepcopy(t1)
t1 is t2
>>> t1 is t2
False
>>>

tuple の中に tuple がはいったオブジェクトは「完全に変更できるない」ので singleton として扱われます。

import copy
t1 = ((), )
t2 = copy.deepcopy(t1)
t1 is t2
>>> t1 is t2
True
>>>

なぜこのようになっているのでしょうか? 以下のような例を考えるとわかりやすいかもしれません。

t1 = ([], )
# 単純に同じものを返しただけでは...
t2 = t1
# t1 を変更すると
t1[0].append(None)
t1
# t2 も変更されてしまう
t2
>>> t1
([None],)
>>> t2
([None],)
>>>

これでもピンとこない方は、 以下の記事をご参照していただくとお力添えできるかもしれません。

# 4.3. int のインスタンス化について

まったく関係ないですが 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]
>>> # - 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. オブジェクト、値、および型 (opens new window)




# 5. おわりに

identity という言葉を理解するために 「束縛」、「イミュータブル」、「リストの初期化」、「コピー」の4つの概念について触れてきました。 標準ライブラリの copy については、 いつか直接コードを解説するような記事を書いてみたいなと思っています。