Python

最終更新日: 2023.02.16 (公開: 2023.02.13)

Pythonのcopy関数の使い方を解説!注意点やdeepcopyとの違いも

Pythonのcopy関数の使い方を解説!注意点やdeepcopyとの違いも

Pythonでオブジェクトをコピーするとき、「=演算子」による代入を行っていないでしょうか。実は、オブジェクトの種類によっては、代入によるコピーは思わぬ動作の原因になることがあります。具体的には、一方のオブジェクトに変更を加えると、他方のオブジェクトにも影響が及んでしまうのです。

これを防ぐために役立つのが「copy関数」です。copy関数は、オブジェクトの中身を正確にコピーできます。ただし、copy関数でも不十分なケースもあり、そのときは「deepcopy関数」が必要です。本記事では、copy関数の使い方と注意点に加えて、deepcopy関数との違いについても解説します。

Pythonの「copy」関数・メソッドとは?

Pythonの「copy」関数・メソッドとは?

Pythonの「copy関数」は、オブジェクトをコピーするためのメソッドです。しかし、オブジェクトのコピーは、「=演算子」でも行えるはずです。これは「代入」とも呼ばれますが、実はオブジェクトの種類によっては、代入で不都合が生じることがあります。以下のサンプルコードで検証してみましょう。

//サンプルプログラム

# coding: Shift_JIS

# さまざまな変数を用意する
i1 = 1
f1 = 0.1
s1 = "abc"

t1 = (1, 2, 3)
l1 = [10, 20, 30]
d1 = {"k1":100, "k2":200, "k3":300}

# それぞれの変数を「=演算子」でコピーする
i2 = i1
f2 = f1
s2 = s1

t2 = t1
l2 = l1
d2 = d1

# コピー元の変数に変更を加える
i1 = -1
f1 = -0.1
s1 = "xyz"

t1 = (-1, -2, -3)

l1[0] = -10
l1[1] = -20
l1[2] = -30

d1["k1"] = -100
d1["k2"] = -200
d1["k3"] = -300

# コピー元・コピー先の中身を比較する
print(f"コピー元:{i1} コピー先:{i2}")
print(f"コピー元:{f1} コピー先:{f2}")
print(f"コピー元:{s1} コピー先:{s2}")

print(f"コピー元:{t1} コピー先:{t2}")
print(f"コピー元:{l1} コピー先:{l2}")
print(f"コピー元:{d1} コピー先:{d2}")

 

//実行結果

python-copy

上記のように、整数型・浮動小数型・文字列型・タプル型については、コピー元の値を変更してもコピー先に影響はおよびません。これは、「=演算子」でコピーしたときに想定したとおりの動作となるでしょう。

しかし、リスト型と辞書型は様子が異なります。コピー元の内容を変更すると、コピー先の中身も変わってしまいます。つまり、これらのデータ型では、一方を変更すると他方にも影響が及ぶということです。

こうした違いの原因は、「イミュータブルなオブジェクト」と「ミュータブルなオブジェクト」という、オブジェクトの種類にあります。

「イミュータブル」と「ミュータブル」とは?

「イミュータブル」と「ミュータブル」とは?

「イミュータブルなオブジェクト」とは、作成後に変更できないものを指します。一方、「ミュータブルなオブジェクト」は自由に変更できます。Pythonの基本的なデータ型を、イミュータブルとミュータブルに分類してみましょう。

オブジェクトの分類 該当するデータ型
イミュータブルなオブジェクト bool
int
float
complex
str
tuple
range
bytes
file object
ミュータブルなオブジェクト list
dict
set
bytearray
自作クラス

上記のように、ミュータブルなオブジェクトは種類が限られています。しかし、ここで疑問が生じるのではないでしょうか。イミュータブル(変更不可)なオブジェクトであるはずの「int型」や「str型」は、いつでも自由に値を変更できるはずです。実は「変更時のオブジェクトの状態」が、この分類のカギを握っています。

イミュータブルなオブジェクトは変更時に「ID」が変わる

イミュータブルとミュータブルの違いは、「値を変更したときのオブジェクトの状態」にあります。両者の違いを検証するために、「id関数」で変更前後のオブジェクトIDを調べてみましょう。

//サンプルプログラム

# coding: Shift_JIS

# さまざまな変数を用意する
i1 = 1
f1 = 0.1
s1 = "abc"

t1 = (1, 2, 3)
l1 = [10, 20, 30]
d1 = {"k1":100, "k2":200, "k3":300}

# それぞれのオブジェクトのIDを調べる
print(f"i1のID = {id(i1)}")
print(f"f1のID = {id(f1)}")
print(f"s1のID = {id(s1)}")

print(f"t1のID = {id(t1)}")
print(f"l1のID = {id(l1)}")
print(f"d1のID = {id(d1)}")

# それぞれのオブジェクトに変更を加える
i1 = -1
f1 = -0.1
s1 = "xyz"

t1 = (-1, -2, -3)

l1[0] = -10
l1[1] = -20
l1[2] = -30

d1["k1"] = -100
d1["k2"] = -200
d1["k3"] = -300

print()

# もう一度オブジェクトのIDを調べる
print(f"i1のID = {id(i1)}")
print(f"f1のID = {id(f1)}")
print(f"s1のID = {id(s1)}")

print(f"t1のID = {id(t1)}")
print(f"l1のID = {id(l1)}")
print(f"d1のID = {id(d1)}")

 

//実行結果

python-copy

Pythonでは、すべてのオブジェクトに固有のIDが割り当てられています。上記の実行結果のように、イミュータブルなオブジェクトは、値の変更後にIDが変わっていることが理解できるでしょう。一方、ミュータブルなオブジェクトのIDは、値の変更後もそのままです。

つまり、イミュータブルなオブジェクトは、値を変更するときに新しいオブジェクトが作成されるため、厳密には「変更不可」ということです。ミュータブルなオブジェクトは、「[]演算子」やオブジェクトのメンバ関数による値の変更を行う限りは、既存がそのまま維持されます。

ミュータブルなオブジェクトでも「初期化」するとIDが変わる

ミュータブルなオブジェクトは、変更を加えたとしてもIDはそのままです。ただし、以下のように初期化構文を使用すると、ミュータブルなオブジェクトであっても別のオブジェクトに置き換えられます。

//サンプルプログラム

# coding: Shift_JIS

# さまざまな変数を用意する
l1 = [10, 20, 30]
d1 = {"k1":100, "k2":200, "k3":300}

# それぞれのオブジェクトのIDを調べる
print(f"l1のID = {id(l1)}")
print(f"d1のID = {id(d1)}")

# それぞれのオブジェクトに変更を加える
l1 = [-10, -20, -30]
d1 = {"k1":-100, "k2":-200, "k3":-300}

# もう一度オブジェクトのIDを調べる
print()
print(f"l1のID = {id(l1)}")
print(f"d1のID = {id(d1)}")

 

//実行結果

python-copy

重要なポイントは、ミュータブルなオブジェクトのメソッドやインデックスを使用し、中身を変更したときにIDが変わらないことです。イミュータブルなオブジェクトには、そもそもそのような機能が備わっていません。たとえば、タプルを「tuple[0] = 100」のようにインデックスで変更することはできず、初期化構文ですべて置き換える必要があります。

Pythonの「copy関数」の使い方

Pythonの「copy関数」の使い方

「リスト」や「辞書」などをコピーするとき、「=演算子」による代入では、他方を変更するときに不具合が生じます。そのため、ミュータブルなオブジェクトをコピーするときは、「copy関数」を使うようにしましょう。copy関数の構文は以下のとおりです。

コピー先変数 = copy.copy(コピー元変数)

なお、copy関数を使用するためには、「copyライブラリ」をインポートする必要があります。実際に、以下のサンプルコードでミュータブルなオブジェクトをコピーしてみましょう。

//サンプルプログラム

# coding: Shift_JIS

# copyライブラリを使用する
import copy

# リストと辞書を用意する
l1 = [10, 20, 30]
d1 = {"k1":100, "k2":200, "k3":300}

# 「=演算子」でオブジェクトをコピーする
l2 = l1
d2 = d1

# copy関数でオブジェクトをコピーする
l3 = copy.copy(l1)
d3 = copy.copy(d1)

# それぞれのオブジェクトに変更を加える
l1[0] = -10
l1[1] = -20
l1[2] = -30

d1["k1"] = -100
d1["k2"] = -200
d1["k3"] = -300

# 各オブジェクトの中身を比較する
print(f"l1 = {l1} l2 = {l2} l3 = {l3}")
print(f"d1 = {d1} d2 = {d2} d3 = {d3}")

# 各オブジェクトのIDを比較する
print(f"l1 = {id(l1)} l2 = {id(l2)} l3 = {id(l3)}")
print(f"d1 = {id(d1)} d2 = {id(d2)} d3 = {id(d3)}")

 

//実行結果

python-copy

上記のように、元の変数と「=演算子」でコピーした変数は、IDが同じであることがわかります。実際には同じオブジェクトを参照しているため、一方に変更を加えると、他方の中身も変わってしまいます。

ミュータブルなオブジェクトは、値の変更時に新規オブジェクトに置き換えられることがありません。コピー先のオブジェクトも同じものを参照しているため、そちらの中身も書き換えられてしまいます。これが、「=演算子」でミュータブルなオブジェクトをコピーすると、値の変更時に不具合が生じる原因です。

一方、copy関数でコピーした場合は、IDが変わることがポイントです。新規オブジェクトが作成されたうえで中身がコピーされるため、コピー元の中身を変えても影響を受けません。

オブジェクト内部に「ミュータブルなオブジェクト」がある場合は?

ミュータブルなオブジェクトは、copy関数を使うことで、中身まで正確にコピーできることがわかりました。しかし、「オブジェクト内部にミュータブルなオブジェクトがある」場合は例外です。

たとえば、自作クラスのメンバ変数にリストがあるときや、2次元リストをコピーするときなどは、copy関数を使用したとしても不具合が生じます。詳細を以下のサンプルコードで検証してみましょう。

//サンプルプログラム

# coding: Shift_JIS

# copyライブラリを使用する
import copy

# テスト用クラスを定義する
class Test:
  def __init__(self):
    # イミュータブルなメンバ変数
    self.num = 0

    # ミュータブルなメンバ変数
    self.list = [1, 2, 3]
  
# クラスのインスタンスを生成する
t1 = Test()

# copy関数でコピーする
t2 = copy.copy(t1)

# 元のインスタンスのメンバ変数を変更する
t1.num = -1

t1.list[0] = -100
t1.list[1] = -200
t1.list[2] = -300

# それぞれの中身を比較する
print(f"t1(ID:{id(t1)}) = {t1.num}, {t1.list}")
print(f"t2(ID:{id(t2)}) = {t2.num}, {t2.list}")

 

//実行結果

python-copy

非常に奇妙な現象が起きているのではないでしょうか。copy関数を使用しているため、たしかにIDが異なる別オブジェクトとしてコピーできています。イミュータブルな「num変数」も、コピー元が変更されても影響を受けていません。

しかし、問題はミュータブルな「list変数」です。copy関数を使用したにもかかわらず、中身が変更されてしまっています。2次元リストの場合も同様に、copy関数を使用してもうまくいきません。

//サンプルプログラム

# coding: Shift_JIS

# copyライブラリを使用する
import copy

# 2次元リストを作成する
dl1 = [[1, 2], [3, 4]]

# copy関数でコピーする
dl2 = copy.copy(dl1)

# コピー元の変数に変更を加える
dl1[0][0] = -100
dl1[0][1] = -200
dl1[1][0] = -300
dl1[1][1] = -400

# それぞれの中身を比較する
print(f"t1(ID:{id(dl1)}) = {dl1}")
print(f"t2(ID:{id(dl2)}) = {dl2}")

 

//実行結果

python-copy

このように、別オブジェクトとしてコピーしているにもかかわらず、やはり中身が書き換えられてしまいます。これは、copy関数が「シャローコピー(浅いコピー)」を行う関数だからです。

深い階層までコピーする場合は「deepcopy関数」が必須

深い階層までコピーする場合は「deepcopy関数」が必須

前述した問題は、copy関数が対応できる「階層の深さ」に原因です。実は、copy関数がコピーするのは最初の階層のみで、オブジェクト内部のオブジェクトに関しては「代入」を行います。

そのため、オブジェクト内部にミュータブルなオブジェクトがある場合は、そこだけ「=演算子」と同じ結果となります。これが、copy関数が「浅いコピー」と呼ばれる理由です。

より深い階層までコピーする場合は、「deepcopy関数」が必要です。deepcopyはその名のとおり「ディープコピー(深いコピー)」を行うため、オブジェクト内のミュータブルなオブジェクトも、問題なくコピーできます。構文は先ほどのcopy関数と同じなので、さっそくサンプルコードで確認してみましょう。

//サンプルプログラム

# coding: Shift_JIS

# copyライブラリを使用する
import copy

# テスト用クラスを定義する
class Test:
  def __init__(self):
    # イミュータブルなメンバ変数
    self.num = 0

    # ミュータブルなメンバ変数
    self.list = [1, 2, 3]
  
# クラスのインスタンスを生成する
t1 = Test()

# deepcopy関数でコピーする
t2 = copy.deepcopy(t1)

# 元のインスタンスのメンバ変数を変更する
t1.num = -1

t1.list[0] = -100
t1.list[1] = -200
t1.list[2] = -300

# それぞれの中身を比較する
print(f"t1(ID:{id(t1)}) = {t1.num}, {t1.list}")
print(f"t2(ID:{id(t2)}) = {t2.num}, {t2.list}")

 

//実行結果

python-copy

上記のように、Testクラスは内部にミュータブルなメンバ変数を持ちますが、deepcopy関数による深いコピーを行うと、元の変数を変更しても影響を受けません。2次元リストについても調べてみましょう。

//サンプルプログラム

# coding: Shift_JIS

# copyライブラリを使用する
import copy

# 2次元リストを作成する
dl1 = [[1, 2], [3, 4]]

# deepcopy関数でコピーする
dl2 = copy.deepcopy(dl1)

# コピー元の変数に変更を加える
dl1[0][0] = -100
dl1[0][1] = -200
dl1[1][0] = -300
dl1[1][1] = -400

# それぞれの中身を比較する
print(f"t1(ID:{id(dl1)}) = {dl1}")
print(f"t2(ID:{id(dl2)}) = {dl2}")

 

//実行結果

python-copy

2次元リストはリスト内に別のリストを含むため、いわば「ミュータブルなメンバを含むオブジェクト」です。上記のように、deepcopy関数による深いコピーを行うことで、こちらも問題なくコピーできます。

3次元以降の深い階層もdeepcopy関数でOK!

クラス内部に2次元リストがある場合や、3次元リストなどさらに深い階層を含む場合でも、deepcopy関数を使うと正しくコピーできます。

//サンプルプログラム

# coding: Shift_JIS

# copyライブラリを使用する
import copy

# テスト用クラスを定義する
class Test:
  def __init__(self):
    # イミュータブルなメンバ変数
    self.num = 0

    # ミュータブルなメンバ変数
    self.list = [1, 2, 3]

    # ミュータブルなオブジェクトの中に、ミュータブルなオブジェクトを含む2次元リスト
    self.double_list = [[1, 2], [3, 4]]
  
# クラスのインスタンスを生成する
t1 = Test()

# deepcopy関数でコピーする
t2 = copy.deepcopy(t1)

# 元のインスタンスのメンバ変数を変更する
t1.num = -1

t1.list[0] = -100
t1.list[1] = -200
t1.list[2] = -300

t1.double_list[0][0] = -100
t1.double_list[0][1] = -200
t1.double_list[1][0] = -300
t1.double_list[1][1] = -400

# それぞれの中身を比較する
print(f"t1(ID:{id(t1)}) = {t1.num}, {t1.list}, {t1.double_list}")
print(f"t2(ID:{id(t2)}) = {t2.num}, {t2.list}, {t2.double_list}")

 

//実行結果

python-copy

deepcopy関数は、深い階層までたどってオブジェクトをコピーするので、上記のようにネストが深いオブジェクトもすべてコピーできます。元のオブジェクトの内容を変更・削除したとしても、その影響をまったく受けません。同じように、3次元配列もdeepcopy関数でコピーできます。

//サンプルプログラム

# coding: Shift_JIS

# copyライブラリを使用する

import copy

# 3次元リストを作成する
dt1 = [
        [[  1,   2,   3], [  4,   5,   6], [  7,   8,   9]],
        [[ 10,  20,  30], [ 40,  50,  60], [ 70,  80,  90]],
        [[100, 200, 300], [400, 500, 600], [700, 800, 900]],
      ]

# deepcopy関数でコピーする
dt2 = copy.deepcopy(dt1)

# コピー元の変数に変更を加える
dt1[0][0][0] = -1
dt1[0][0][1] = -2
dt1[0][0][2] = -3

dt1[0][1][0] = -4
dt1[0][1][1] = -5
dt1[0][1][2] = -6

dt1[0][2][0] = -7
dt1[0][2][1] = -8
dt1[0][2][2] = -9

dt1[1][0][0] = -10
dt1[1][0][1] = -20
dt1[1][0][2] = -30

dt1[1][1][0] = -40
dt1[1][1][1] = -50
dt1[1][1][2] = -60

dt1[1][2][0] = -70
dt1[1][2][1] = -80
dt1[1][2][2] = -90

dt1[2][0][0] = -100
dt1[2][0][1] = -200
dt1[2][0][2] = -300

dt1[2][1][0] = -400
dt1[2][1][1] = -500
dt1[2][1][2] = -600

dt1[2][2][0] = -700
dt1[2][2][1] = -800
dt1[2][2][2] = -900

# それぞれの中身を比較する
print(f"t1(ID:{id(dt1)}) = {dt1}")
print(f"t2(ID:{id(dt2)}) = {dt2}")

 

//実行結果

python-copy

以上のように、2次元以上の深い階層にミュータブルなオブジェクトがある場合は、基本的にdeepcopy関数による深いコピーが必須だといえるでしょう。

標準関数・メソッドは「浅いコピー」が多いので要注意

標準関数・メソッドは「浅いコピー」が多いので要注意

前述したように、「copy関数(浅いコピー)」と「deep関数(深いコピー)」は、オブジェクト内部にミュータブルなオブジェクトがある場合の挙動が異なります。

Pythonで標準的に搭載されている関数・メソッドの中には、copy関数と同じく「浅いコピー」のものが多いので注意が必要しましょう。たとえば、リストや辞書などの「copyメソッド」などは、以下のサンプルコードのように浅いコピーを行う代表例です。

//サンプルプログラム

# coding: Shift_JIS

# リストの作成とコピーを行う
l1 = [0, 1, [2, 3]]
l2 = l1.copy()

# コピー元の変数に変更を加える
l1[1] = 100
l1[2][0] = 200
l1[2][1] = 300

# それぞれの中身を比較する
print(f"l1(ID:{id(l1)}) = {l1}")
print(f"l2(ID:{id(l2)}) = {l2}")

 

//実行結果

python-copy

1次元リストの部分は、元の変数を変更しても影響を受けませんが、リスト内部にリストがある部分は変更されてしまいます。「l2 = list(l1)」のように「list関数」にリストを渡してコピーする場合や、「l2 = l1[:]」とスライスする場合も同じです。正しくコピーしたい場合は、前述したようにdeepcopy関数による深いコピーを行いましょう。

「copy関数(浅いコピー)」と「deepcopy関数(深いコピー)」の使い分け

「copy関数(浅いコピー)」と「deepcopy関数(深いコピー)」の使い分け

基本的には、「浅いコピー」を行うcopy関数で十分です。オブジェクト内にミュータブルなオブジェクトがないのであれば、「深いコピー」を行うdeepcopy関数の必要性がありません。たとえば、リストをソートするときに元のリストを保持したいのであれば、以下のようにcopy関数を使えばOKです。

//サンプルプログラム

# coding: Shift_JIS

# copyライブラリを使用する
import copy

# リストを作成する
l1 = [3, 1, 4, 5, 2]

# 「=演算子」で代入する
l2 = l1

# copy関数でコピーする
l3 = copy.copy(l1) # l3 = l1.copy() でもOK

# deepcopy関数でコピーする
l4 = copy.deepcopy(l1)

# 元のリストをソートする
l1.sort()

# それぞれの中身を比較する
print(f"l1(ID:{id(l1)}) = {l1}")
print(f"l2(ID:{id(l2)}) = {l2}")
print(f"l3(ID:{id(l3)}) = {l3}")
print(f"l4(ID:{id(l4)}) = {l4}")

 

//実行結果

python-copy

copy関数もdeepcopy関数も結果は同じなので、浅いコピーのcopy関数で十分です。ただし、オブジェクトの内部にリストがあるなど、2次元以降の階層もコピーする必要がある場合も。その際は必ず深いコピーのdeepcopy関数を使うようにしましょう。

Pythonの「copy関数(浅いコピー)」と「deepcopy関数(深いコピー)」をうまく使い分けよう!

Pythonの「copy関数(浅いコピー)」と「deepcopy関数(深いコピー)」をうまく使い分けよう!

Pythonでは、リストや辞書などのミュータブルなオブジェクトを「=演算子」でコピーすると、元の変数を変更したときにコピー先の中身も変わってしまいます。そのため、copy関数によるコピーが必要です。

ただし、copy関数で行われるのは「浅いコピー」なので、オブジェクト内にミュータブルなオブジェクトがある場合は、変更の影響を受けます。より深い階層まで正しくコピーしたい場合は、deepcopy関数による「深いコピー」を行いましょう。オブジェクトの中身に応じて、copy関数(浅いコピー)とdeepcopy(深いコピー)を使い分けることが重要です。

アクセスランキング 人気のある記事をピックアップ!

    コードカキタイがオススメする記事!

    1. 子供におすすめのプログラミングスクール10選!学習メリットや教室選びのコツも紹介

      2024.01.26

      子供におすすめのプログラミングスクール10選!学習メリットや教室選びのコツも紹介

      #プログラミングスクール

    2. 【完全版】大学生におすすめのプログラミングスクール13選!選ぶコツも詳しく解説

      2022.01.06

      【完全版】大学生におすすめのプログラミングスクール13選!選ぶコツも詳しく解説

      #プログラミングスクール

    3. 【未経験でも転職可】30代におすすめプログラミングスクール8選!

      2024.01.26

      【未経験でも転職可】30代におすすめプログラミングスクール8選!

      #プログラミングスクール

    4. 初心者必見!独学のJava学習方法とおすすめ本、アプリを詳しく解説

      2024.01.26

      初心者必見!独学のJava学習方法とおすすめ本、アプリを詳しく解説

      #JAVA

    5. 忙しい社会人におすすめプログラミングスクール15選!失敗しない選び方も詳しく解説

      2024.01.26

      忙しい社会人におすすめプログラミングスクール15選!失敗しない選び方も詳しく解説

      #プログラミングスクール

    1. 【無料あり】大阪のおすすめプログラミングスクール14選!スクール選びのコツも紹介

      2022.01.06

      【無料あり】大阪のおすすめプログラミングスクール14選!スクール選びのコツも紹介

      #プログラミングスクール

    2. 【目的別】東京のおすすめプログラミングスクール20選!スクール選びのコツも徹底解説

      2024.01.26

      【目的別】東京のおすすめプログラミングスクール20選!スクール選びのコツも徹底解説

      #プログラミングスクール

    3. 【無料あり】福岡のおすすめプログラミングスクール13選!選び方も詳しく解説

      2024.01.26

      【無料あり】福岡のおすすめプログラミングスクール13選!選び方も詳しく解説

      #プログラミングスクール

    4. 【徹底比較】名古屋のおすすめプログラミングスクール13選!選び方も詳しく解説

      2024.01.26

      【徹底比較】名古屋のおすすめプログラミングスクール13選!選び方も詳しく解説

      #プログラミングスクール

    5. 【徹底比較】おすすめのプログラミングスクール18選!失敗しない選び方も徹底解説

      2024.01.26

      【徹底比較】おすすめのプログラミングスクール18選!失敗しない選び方も徹底解説

      #プログラミングスクール