Python♪「参照渡し」「浅いコピー」「深いコピー」まずは理屈抜きで覚えよう。

 「参照渡し」「浅いコピー」「深いコピー」がどんな結果になるかを、まずは理屈抜きで覚えましょう。数値型、タプル型、文字列、リスト、ディクショナリ、セットがどんな風になるのかを具体的に細かく説明した記事は、なかなか見つけられませんでしたので、この記事で説明します。

 さて、「参照渡し」「浅いコピー」「深いコピー」は、きちんと理解していなければ怖いです。コード01, 出力01はよく目にする例題です。a = [1]とlistを代入し、b = aで、bも[1]となります。でも、aの内容を変えると、bの内容も変わってしまうのです。aとbは連動しています。

#コード01
a = [1]
b = a
print('a, b = ', a, b)
a[0] = 2
print('a, b = ', a, b)
#出力01
a, b = [1] [1]
a, b = [2] [2]

 一方、listではなく数値型の場合は、aとbは連動せず、それぞれ独立した変数になっています。なぜ、違うの!

#コード02
a = 1
b = a
print('a, b = ', a, b)
a = 2
print('a, b = ', a, b)
#出力02
a, b =  1 1
a, b =  2 1

 これを最初に知ったとき、私は恐怖を覚えました。しかも、どうやら「参照渡し」「浅いコピー」「深いコピー」という3種類のコピーの仕方があるようなのです。この違いの存在を示す記事はよく見かけるのですが、それぞれの型がどうなるのかを詳しく説明している記事は見つけられず、「ひとつひとつ、確かめないと怖くて使えない・・・」というのが正直な感想でした。

変更不能体の「参照渡し」「浅いコピー」「深いコピー」

 変更不能体には、数値型、文字列、リスト(list)、ブール型、フローズンセットがあります。変更不能体は、「参照渡し」「浅いコピー」「深いコピー」の区別を考える必要がありません。b=aという表記だけを覚えればよく、コピーの後、それぞれの変数は独立しています。
 どうですか?ひとまず、半分ぐらいはすっきりしたでしょ?

関連記事:変更可能体(mutable)と変更不能体(immutable)

#コード03
a = 1
b = a  #参照渡し
a = 4
print('数値:', a, b)  #a = 4, b = 1, aとbは連動しない。

a = 'abcd'
b = a  #参照渡し
a = 'efgh'
print('文字列:', a, b)  #a = 'efgh', b = 'abcd', aとbは連動しない。


a = (1, 2)
b = a  #参照渡し
a = (3, 4)
print('タプル:', a, b)  #a = (3, 4), b = (1, 2), aとbは連動しない。
#出力03
数値: 4 1
文字列: efgh abcd
タプル: (3, 4) (1, 2)

 

リストの「参照渡し」「浅いコピー」「深いコピー」

 次はリストの「参照渡し」「浅いコピー」「深いコピー」について説明します。変更不能体よりは覚えることが多いのですが、整理して覚えれば単純です。

「参照渡し」「浅いコピー」「深いコピー」の書式

 それぞれの書式は、以下の通りです。浅いコピーは「スライス」を使ったb=[:]の方が楽ですよね。コロン「:」を忘れないようにしましょう。

#コード04
#参照渡し
b=a

#浅いコピー01
b=a[:]

#浅いコピー02
import copy
b = copy.copy(a)

#深いコピー
import copy
b = copy.deepcopy(a)

 

「変数の初期化」は全てを超越する

 コード05は「参照渡し」b=aを行ったあとに、aの値を変更し、bの値と連動するかを確認しています。
 ただし、ここで重要なのは、aの変更が、① 変数の「要素を」変更しているのか、それとも、② 変数全体を別の値に入れ替え、「変数の再定義」を行っているのかという点です。

 ①変数の要素の変更:ケース01、ケース03
 ②変数の再定義:ケース02、ケース04 ※左辺が「変数名 =」の形になっている。

 「変数の再定義」は、今の場所を消して上書きするのではなく、現在の記憶場所を放置し、別の記憶場所に定義し直します。つまり、例えばケース02ではa = [5]で、aはそれまでのbと共有していた記憶場所のことは放置したまま、別の記憶場所に定義し直します。だから、aとbは全く別の独立した変数になるのです。

 つまり、bにaを「参照渡し」「浅いコピー」「深いコピー」のいずれの方法でコピーしたとしても、「変数を再定義」した時点で、aとbは別の独立した変数になります。
 どうでしょうか?さらに、1割ぐらいはすっきりしたのでは?

#コード05

#ケース01
a = [1]
b = a  #参照渡し
a[0]=5 #要素の変更
print('要素の変更:a, b = ',a, b) #連動する

#ケース02
a = [1]
b = a  #参照渡し
a = [5] #変数の再定義
print('変数の再定義:a, b = ',a, b) #それぞれ独立

#ケース03
a = [1, [2], [3, [4]]]
b = a  #参照渡し
a[2] = [7, [8]] #要素の変更
print('要素の変更:a, b = ',a, b) #連動する

#ケース04
a = [1, [2], [3, [4]]]
b = a  #参照渡し
a = [5, [6], [7, [8]]] #変数の再定義
print('変数の再定義:a, b = ',a, b) #それぞれ独立
#出力05
要素の変更:a, b = [5] [5]
変数の再定義:a, b = [5] [1]
要素の変更:a, b = [1, [2], [7, [8]]] [1, [2], [7, [8]]]
変数の再定義:a, b = [5, [6], [7, [8]]] [1, [2], [3, [4]]]

 

「参照渡し」(b=a)

 リストの「参照渡し」はb=aという形で行います。コード05のような「変数の再定義」は例外ですが、参照渡しの後に、aの「要素の」内容を変更すれば、bも変わります。逆にbの「要素を」変更をすれば、aが変わります。「参照渡し」を行うことで、完全に内容が連動する変数ができあがります。

 以下、具体例を示します。コード06の#★★★の行でaを変更する場合、連動してbが変更されるかどうかを整理します。

#コード06
a = [1, [2], [3, [4]]]
b=a #参照渡し
#★★★この行に、以下のようなaを変更するコードを記述します。
print('a, b =', a, b)

1.aとbが連動しないケース
 (1) 「変数の再定義」の時のみ連動しない。 ※左辺が「変数名 =」の形になっている。
  a = [5, [6], [7, [8]]]  

2.aとbが連動するケース 
   (1) 上記「変数の再定義」の時以外
  a[0] = 5,   a[1] = [6],   a[2] = [7, [8]], 
      a[1][0] = 6,   a[2][0] = 7,   a[2][1]=[8],   a[2][1][0] = 8

 

「浅いコピー」(shallow copy, シャローコピー)

 リストの「浅いコピー」は、b=a[:], あるいはimport copy;  b=copy.copy(a)の書式で行います。浅いコピーは一番上の要素を入れ替える場合は連動しませんが、リストの中にリストが入っているような場合、深い部分の要素を変更する場合には連動します。

 以下、具体例を示します。コード07の#★★★の行でaを変更する場合、連動してbが変更されるかどうかを整理します。

#コード07
a = [1, [2], [3, [4]]]
b=a[:] #浅いコピー
#★★★この行に、以下のようなaを変更するコードを記述します。
print('a, b =', a, b)

1.aとbが連動しないケース
 (1) 「変数の再定義」
  a = [5, [6], [7, [8]]]  
 (2)リストの一番上の要素を変更
  a[0] = 5,   a[1] = [6],   a[2] = [7, [8]] 

2.aとbが連動するケース
   (1) 深い部分の要素を変更
  a[1][0] = 6,   a[2][0] = 7,   a[2][1]=[8],  a[2][1][0] = 8

 リストではa=[5, 3, 4, 2]とか、a=['りんご', 'みかん', 'イチゴ']のように、リストの要素にリストを含まない形が多いと思います。このような場合には「浅いコピー」により、それぞれ独立した変数にすることが可能です。

「深いコピー」(deep copy, ディープコピー)

 リストの「深いコピー」は、import copy;  b=copy.deepcopy(a)の書式で行います。深いコピーは、すべての変更が連動しない、それぞれ独立した変数にすることができます。

 以下、具体例を示します。コード08の#★★★の行でaを変更する場合、連動してbが変更されるかどうかを整理します。

#コード08
import copy
a = [1, [2], [3, [4]]]
b=copy.deepcopy(a) #深いコピー
#★★★この行に、以下のようなaを変更するコードを記述します。
print('a, b =', a, b)

1.aとbが連動しないケース
  (1) 「深いコピー」では全ての変更が連動しません。
  a = [5, [6], [7, [8]]] , 
  a[0] = 5,   a[1] = [6],   a[2] = [7, [8]], 
      a[1][0] = 6,   a[2][0] = 7,   a[2][1]=[8],   a[2][1][0] = 8

ディクショナリ型の「参照渡し」「浅いコピー」「深いコピー」

 リストと同じなので、説明は省略します。ただ、ディクショナリ型ではスライスを使ったコピー  b=a[:]という表記はできません。以下、コード09を参考にしてください。
(参考:スライスが使えないのは、ディクショナリ型は要素に順番という概念がないため、要素の何番目から何番目といった指定ができないからです。)

#コード09
import copy

a = {'a':{'e':44}, 'b':22}
b = a #参照渡し
a['b']=55
print("参照渡し、a['b']=55:",a, b) #連動する

a = {'a':{'e':44}, 'b':22}
b = copy.copy(a) #浅いコピー b=a[:]という表記はできない。
a['b']=55
print("浅いコピー、a['b']=55:",a, b) #それぞれ独立

a = {'a':{'e':44}, 'b':22}
b = copy.deepcopy(a) #
a['b']=55
print("深いコピー、a['b']=55:",a, b) #それぞれ独立

a = {'a':{'e':44}, 'b':22}
b = a #参照渡し
a['a']['e']=55
print("参照渡し、a['a']['e']=55:",a, b) #連動する

a = {'a':{'e':44}, 'b':22}
b = copy.copy(a) #浅いコピー b=a[:]という表記はできない。
a['a']['e']=55
print("浅いコピー、a['a']['e']=55:",a, b) #連動する

a = {'a':{'e':44}, 'b':22}
b = copy.deepcopy(a) #深いコピー
a['a']['e']=55
print("深いコピー、a['a']['e']=55:",a, b) #それぞれ独立
#出力09
参照渡し、a['b']=55: {'a': {'e': 44}, 'b': 55} {'a': {'e': 44}, 'b': 55}
浅いコピー、a['b']=55: {'a': {'e': 44}, 'b': 55} {'a': {'e': 44}, 'b': 22}
深いコピー、a['b']=55: {'a': {'e': 44}, 'b': 55} {'a': {'e': 44}, 'b': 22}
参照渡し、a['a']['e']=55: {'a': {'e': 55}, 'b': 22} {'a': {'e': 55}, 'b': 22}
浅いコピー、a['a']['e']=55: {'a': {'e': 55}, 'b': 22} {'a': {'e': 55}, 'b': 22}
深いコピー、a['a']['e']=55: {'a': {'e': 55}, 'b': 22} {'a': {'e': 44}, 'b': 22}

 しかし、ディクショナリ型の中にディクショナリ型を入れるなんてことが実際にあるのかな?

 

setの「参照渡し」「浅いコピー」「深いコピー」

 setもリストと同じ考え方で理解できますが、リストと違い変更不能体しか要素にすることができません。したがって、入れ子にすることができないため、setには要素に深い部分がありません。つまり、「浅いコピー」でも「深いコピー」でも、それぞれ完全に独立した変数となります。わざわざ「深いコピー」使う意味もありません。
 なお、ディクショナリ型と同様にスライスを使ったコピーb=a[:]という表記はできません。

(参考1:setは変更可能体であるsetを要素にすることはできません。)
(参考2:変更不能体のタプルは要素にすることができます。しかし、タプルの要素は変更できませんから、タプル全体を変更するしかなく、深い部分は存在しません。)

#コード10
import copy

a = {1,2}
b = a
a = {3,4}
print("参照渡し、a = {3,4}:",a,b) #別のsetに初期化

a = {1,2}
b = a
a.add(4)
print("参照渡し、a.add(4):",a,b) #連動する

a = {1,2}
b = copy.copy(a)
#b = a[:]  #エラー
a.add(4)
print("浅いコピー、a.add(4):",a,b) #それぞれ独立

a = {1,2}
b = copy.deepcopy(a)
a.add(4)
print("深いコピー、a.add(4):",a,b) #それぞれ独立
#出力10
参照渡し、a = {3,4}: {3, 4} {1, 2}
参照渡し、a.add(4): {1, 2, 4} {1, 2, 4}
浅いコピー、a.add(4): {1, 2, 4} {1, 2}
深いコピー、a.add(4): {1, 2, 4} {1, 2}

 

まとめ

1.変更不能体のコピー

 変更不能体には、数値型、文字列、リスト(list)、ブール型、フローズンセットがあります。変更不能体は「参照渡し」「浅いコピー」「深いコピー」の、どのコピーを行ったとしても、それぞれ独立した変数として扱えますので、一番簡単で、速く、メモリーも節約できるa = bの書式を用います。

 変更不能体は変更できないので、変数の内容を変えたいときは、変更ではなく、変数を「再定義」するしかありません。「変数の再定義」は、今の場所を消して上書きするのではなく、現在の記憶場所を放置し、別の記憶場所に定義し直します。
 例えば、「参照渡し」(a = 3 →  b = a)では、aとbは同じ場所に記憶された3を共有します。しかし、aを再定義すると、3を記憶したbをそのまま放置し、aは全く別の場所に記憶された新しい値を覚え直します。だから、「参照渡し」だとしても、それぞれ、独立した変数と見なすことができるのです。

関連記事:変更可能体(mutable)と変更不能体(immutable)

2.リストの「参照渡し」「浅いコピー」「深いコピー」

 具体例でまとめます。a = [5, [6], [7, [8]]]  を、bに「参照渡し」「浅いコピー」「深いコピー」した場合の例を示します。

(1) 要点

(a)「参照渡し」(b=a)をした後に、aかbのどちらかの「要素を」変更すると、他方の変数の値も連動して変更されます。ただし、変数全体を入れ替えると、それは「変数の再定義」となるので連動しません。

(b)「浅いコピー」(b=a[:], b=copy.copy(a))を行った後に、aかbのどちらかの「一番上の要素」を変更しても、他方の変数の値は連動して変更されません。しかし、それよりも深い部分の要素を変更すると、他方の変数は連動して変更されます。なお、変数全体を入れ替えると、それは「変数の再定義」となるので連動しません。

(c)「深いコピー」(b=copy.deepcopy(a))した後に、aやbにどんな変更を加えても他方の変数に影響をあたえず、それぞれ、完全に独立した変数です。

※「参照渡し」「浅いコピー」「深いコピー」のいずれの場合も変数全体を入れ替え、「変数の再定義」を行うと連動しません。
※ 「変数の再定義」は左辺が「変数名 =」の形になっています。

(2) 具体例の図示

(a) 参照渡し

 下の図では、参照渡し(b=a)とした後、リストaの要素を変更した場合に、連動してリストbの値が変更されるかどうかを実線と点線で区別しています。
 例えば、参照渡し(b=a)とした後に、a = [ 5, [6], [7, [8]] ] とすると、bは連動して変更されません。したがって、図のa=の部分は実線になっています。
 一方、参照渡し(b=a)とした後に、a[0] = 5とすると、bも連動して、b = [ 5, [2], [3, [4]] ]となるので、図のa[0]=の部分は点線になっています。

(b) 浅いコピー

 下の図では、浅いコピー(b=a[:])とした後、リストaの要素を変更した場合に、連動してリストbの値が変更されるかどうかを実線と点線で区別しています。
 例えば、浅いコピー(b=a[:])とした後に、a[1] = [5] とすると、bは連動して変更されません。したがって、図のa[1]=の部分は実線になっています。
 一方、浅いコピー(b=a[:])とした後に、a[1][0] = 5とすると、bも連動して、b = [ 1, [5], [3, [4]] ]となるので、図のa[1][0] =の部分は点線になっています。
 つまり、浅いコピーでは一番浅い部分の要素の変更a[0]=, a[1]=, a[2]=ではコピー先とコピー元が連動しません。

(c) 深いコピー

 図を見るまでもなく、深いコピー後は、aやbのどの様な変更に対しても他方は連動して変更されたりしないので、図は全て実線になっています。

 

3.ディクショナリは、リストとほぼ同じ

(1) 浅いコピーでb=a[:]の表記はできないため、b=copy.copy(a)とする必要がある。
(2) その他はリストと同じ。

4.setは「浅いコピー」と「深いコピー」の結果が同じ

(1) 浅いコピーでb=a[:]の表記はできないため、b=copy.copy(a)とする必要がある。
(2) 「参照渡し」(b = a)はリストと同じ。
(3) 変更不能体しか要素にできないため、setには深い部分がない。つまり、「浅いコピー」「深いコピー」ともに、それぞれ完全に独立した変数になります。つまり、わざわざ、コピーの遅い「深いコピー」を使用する意味がないので、「浅いコピー」b=copy.copy(a)によりコピーします。

いかがですか。すっきりしましたか?

しか~し、NumPy配列は、リストと考え方が全然違います。

 う~ん、すっきりしませんね。NumPy以外の基本的なものは、だいたい押さえたと思うんですが、やっぱり、Pythonは新しいのを使う前には確認しなくちゃダメかも。NumPy配列も記事書きま~す。(以下、記事を追加しました。)

NumPyの「参照渡し」と「コピー」を覚えよう。リストとは全然違う。