Pythonでオブジェクトをコピーするとき、「=演算子」による代入を行っていないでしょうか。実は、オブジェクトの種類によっては、代入によるコピーは思わぬ動作の原因になることがあります。具体的には、一方のオブジェクトに変更を加えると、他方のオブジェクトにも影響が及んでしまうのです。
これを防ぐために役立つのが「copy関数」です。copy関数は、オブジェクトの中身を正確にコピーできます。ただし、copy関数でも不十分なケースもあり、そのときは「deepcopy関数」が必要です。本記事では、copy関数の使い方と注意点に加えて、deepcopy関数との違いについても解説します。
目次
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の基本的なデータ型を、イミュータブルとミュータブルに分類してみましょう。
オブジェクトの分類 | 該当するデータ型 |
---|---|
イミュータブルなオブジェクト | bool int float complex str tuple range bytes file object |
ミュータブルなオブジェクト | list dict set bytearray 自作クラス |
上記のように、ミュータブルなオブジェクトは種類が限られています。しかし、ここで疑問が生じるのではないでしょうか。イミュータブル(変更不可)なオブジェクトであるはずの「int型」や「str型」は、いつでも自由に値を変更できるはずです。実は「変更時のオブジェクトの状態」が、この分類のカギを握っています。
イミュータブルとミュータブルの違いは、「値を変更したときのオブジェクトの状態」にあります。両者の違いを検証するために、「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では、すべてのオブジェクトに固有の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)}")
//実行結果
重要なポイントは、ミュータブルなオブジェクトのメソッドやインデックスを使用し、中身を変更したときにIDが変わらないことです。イミュータブルなオブジェクトには、そもそもそのような機能が備わっていません。たとえば、タプルを「tuple[0] = 100」のようにインデックスで変更することはできず、初期化構文ですべて置き換える必要があります。
「リスト」や「辞書」などをコピーするとき、「=演算子」による代入では、他方を変更するときに不具合が生じます。そのため、ミュータブルなオブジェクトをコピーするときは、「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)}")
//実行結果
上記のように、元の変数と「=演算子」でコピーした変数は、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}")
//実行結果
非常に奇妙な現象が起きているのではないでしょうか。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}")
//実行結果
このように、別オブジェクトとしてコピーしているにもかかわらず、やはり中身が書き換えられてしまいます。これは、copy関数が「シャローコピー(浅いコピー)」を行う関数だからです。
前述した問題は、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}")
//実行結果
上記のように、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}")
//実行結果
2次元リストはリスト内に別のリストを含むため、いわば「ミュータブルなメンバを含むオブジェクト」です。上記のように、deepcopy関数による深いコピーを行うことで、こちらも問題なくコピーできます。
クラス内部に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}")
//実行結果
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}")
//実行結果
以上のように、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}")
//実行結果
1次元リストの部分は、元の変数を変更しても影響を受けませんが、リスト内部にリストがある部分は変更されてしまいます。「l2 = list(l1)」のように「list関数」にリストを渡してコピーする場合や、「l2 = l1[:]」とスライスする場合も同じです。正しくコピーしたい場合は、前述したように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}")
//実行結果
copy関数もdeepcopy関数も結果は同じなので、浅いコピーのcopy関数で十分です。ただし、オブジェクトの内部にリストがあるなど、2次元以降の階層もコピーする必要がある場合も。その際は必ず深いコピーのdeepcopy関数を使うようにしましょう。
Pythonでは、リストや辞書などのミュータブルなオブジェクトを「=演算子」でコピーすると、元の変数を変更したときにコピー先の中身も変わってしまいます。そのため、copy関数によるコピーが必要です。
ただし、copy関数で行われるのは「浅いコピー」なので、オブジェクト内にミュータブルなオブジェクトがある場合は、変更の影響を受けます。より深い階層まで正しくコピーしたい場合は、deepcopy関数による「深いコピー」を行いましょう。オブジェクトの中身に応じて、copy関数(浅いコピー)とdeepcopy(深いコピー)を使い分けることが重要です。
2024.06.17
子供におすすめのプログラミングスクール10選!学習メリットや教室選びのコツも紹介
#プログラミングスクール
2022.01.06
【完全版】大学生におすすめのプログラミングスクール13選!選ぶコツも詳しく解説
#プログラミングスクール
2024.01.26
【未経験でも転職可】30代におすすめプログラミングスクール8選!
#プログラミングスクール
2024.01.26
初心者必見!独学のJava学習方法とおすすめ本、アプリを詳しく解説
#JAVA
2024.01.26
忙しい社会人におすすめプログラミングスクール15選!失敗しない選び方も詳しく解説
#プログラミングスクール
2022.01.06
【無料あり】大阪のおすすめプログラミングスクール14選!スクール選びのコツも紹介
#プログラミングスクール
2024.01.26
【目的別】東京のおすすめプログラミングスクール20選!スクール選びのコツも徹底解説
#プログラミングスクール
2024.01.26
【無料あり】福岡のおすすめプログラミングスクール13選!選び方も詳しく解説
#プログラミングスクール
2024.01.26
【徹底比較】名古屋のおすすめプログラミングスクール13選!選び方も詳しく解説
#プログラミングスクール
2024.01.26
【徹底比較】おすすめのプログラミングスクール18選!失敗しない選び方も徹底解説
#プログラミングスクール