Python♪次は理屈で覚えよう「参照渡し」「浅いコピー」「深いコピー」

「参照渡し」「浅いコピー」「深いコピー」まずは理屈抜きで覚えよう。】の続編です。今度は、変数のデータのしくみを図示しながら、なぜ、参照渡しはデータが連動し、浅いコピーは深い部分だけが連動するのかを説明します。このしくみがわかれば関数の引数なども理解しやすくなると思いますので、ご一読ください。

さて、続編を書くまで1年もかかってしまいました。最初から書きたかった記事なのですが、どんな図が分かりやすいのか悩みました。浅いコピーを説明しようとすると複雑になっちゃうんですよね。私がExcelで描いた図にご期待ください。

1.参考記事

同じような図を使っていますので、以下の記事も参考にしてください。変数の値の変更について説明しています。この記事への導入にもなりますし、ミュータブル(変更可能体)、イミュータブル(変更不能体)の違いがはっきりします。

本当は変数には参照先のアドレス(id)しか入っていない

2.イミュータブル(変更不能体)の参照渡し

コード01は、3行目で参照渡しを行っています。変数aにはイミュータブル(変更不能体)が代入されているため、その後6行目でbの内容を変更してもaの内容は連動しません。bのidも違うidになっています。Pythonでは有名な例題です。

#コード01
a = 3
b = a   #参照渡し
print('a, b =', a, b)
print('id(a), id(b)=', id(a), id(b))
b = 5
print('a, b =', a, b)
print('id(a), id(b)=', id(a), id(b))
#出力01
a, b = 3 3
id(a), id(b)= 1517121056 1517121056
a, b = 3 5
id(a), id(b)= 1517121056 1517121120

(1) 整数オブジェクトの生成

これを図示します。コード01の3行目の「a = 3」において、最初はまず右辺の3の部分が実行され、整数オブジェクト3が生成されます。この整数オブジェクトは変数aとは独立しており、変数aとは別の場所に記憶されます。

(2) 変数aに参照先を代入

次に「a = 」の部分が実行され、整数オブジェクト3が保存されているアドレス(出力01の3行目の1517121056番地)が変数aに代入されます。変数aには整数ではなく参照先アドレスだけが代入されます。

変数aから整数3を取り出すときには、変数aの参照先アドレス(id)から、整数オブジェクト3が参照されることになります。

(3) 変数bに参照渡し

次に「b = a」では参照渡しが行われます。参照渡しでは変数bに変数aの参照先アドレス(id)がコピーされます。ある意味、純粋に変数aがそのまま変数bにコピーされたとも言えます。

参照先が同じi番地なので、同じ整数オブジェクトを参照します。

(4) 変数bの再定義

同じ整数オブジェクト3を参照しているので、「b = 5」とすると連動してaも変化するように思うかもしれませんが、整数オブジェクト3は変更不能体です。「b = 5」では、i番地の整数オブジェクトを5に変更できないので、整数オブジェクト3はそのまま放置し、別のk番地に整数オブジェクト5を作ります。そして、変数bは参照先をk番地に変更します。出力01を見ると、変数bのidが変更されているのがわかります。

これは、変数bを変更したというよりも、全く新しい変数に再定義したと言えます。

このように、イミュータブル(変更不能体)であるオブジェクトは変更できないので、変数の変更は再定義しかなく、他の変数に影響を与えることはあり得ません。

仕組みがわかれば、イミュータブルが連動しないことが明快にわかります。したがって、イミュータブルの場合、参照渡しであってもお互い完全に独立した変数しか出来ません。つまり、以下に述べる浅いコピーや深いコピーを行う必要はありません。

3.ミュータブル(変更可能体)の参照渡し

コード02も有名な例題です。リストは先ほどの整数と違いミュータブル(変更可能体)です。従って、8行目で変数bの要素の変更をすると、変数aの内容まで連動して変更されます。

#コード02
a = [2, 3]
b = a   #参照渡し
print('a, b =', a, b)
print('id(a), id(b) =', id(a), id(b))
print('id(a[0]), id(a[1]) =', id(a[0]), id(a[1]))
print('id(b[0]), id(b[1]) =',id(b[0]), id(b[1]))
b[0] = 5
print('a, b =', a, b)
print('id(a), id(b) =', id(a), id(b))
print('id(a[0]), id(a[1]) =', id(a[0]), id(a[1]))
print('id(b[0]), id(b[1]) =',id(b[0]), id(b[1]))
#出力02
a, b = [2, 3] [2, 3]
id(a), id(b) = 2379324095816 2379324095816
id(a[0]), id(a[1]) = 1448963584 1448963616
id(b[0]), id(b[1]) = 1448963584 1448963616
a, b = [5, 3] [5, 3]
id(a), id(b) = 2379324095816 2379324095816
id(a[0]), id(a[1]) = 1448963680 1448963616
id(b[0]), id(b[1]) = 1448963680 1448963616

(1) リストオブジェクトの生成

リストの場合も、最初は「a = [2, 3]」の右辺の[2, 3]が実行され、リストオブジェクトが生成されます。

(2) 変数aに参照先を代入

次に「a = [2, 3]」の左辺である変数aにリストオブジェクト[2, 3]が保存されているアドレスが代入されます。

さて、上の図を少し複雑にします。「浅いコピー」を説明するための準備です。実はリストの要素である整数2や整数3はリストに直接入っているわけではなく、別の場所に整数オブジェクトとして独立して存在しています。リストオブジェクトには整数オブジェクトのアドレスm1とm2が代入されているだけなのです。

(3) 変数bに参照渡し

変数bに参照先のアドレスが渡されます。参照渡しなので、同じリストオブジェクトを共有します。ここまでは、先ほどの整数の場合と同じ流れです。

下図も上図を詳しくしたものです。同じようにリストオブジェクトの中に整数が直接代入されているわけではないので本当は下図のようになっています。

(4) 変数bの要素の変更

コード02の8行目では、b[0] = 5により要素が変更されました。左辺は変数名だけではなく要素を示す「[0]」がついていることに注目してください。

リストはミュータブル(変更可能体)なので、要素を変更することが可能です。従って、イミュータブル(変更不能体)の場合とは異なり、リストオブジェクトは参照先アドレスがi番地のまま内容が変更されました。

従って、変数bの変更に対して変数aも連動して内容が変わってしまいます。

上図を詳しくしたものを下図に示します。[2, 3]の2を変更するときm1番地にある整数オブジェクト2はイミュータブル(変更不能体)ですから、「b[0] = 5」により要素の変更を行うと整数オブジェクト2は放置され、m3番地に整数オブジェクト5が再定義されます。

なお、この内容が理解しにくい場合は以下の記事を参考にしてください。
本当は変数には参照先のアドレス(id)しか入っていない

以下、出力02です。(記事の上の方にある出力02と同じものです。)

出力02を見れば、要素の変更の前後でa[0]、b[0]の参照先アドレスがいずれも1448963584番地から1448963680番地に変更されていることがわかります。

#出力02
a, b = [2, 3] [2, 3]
id(a), id(b) = 2379324095816 2379324095816
id(a[0]), id(a[1]) = 1448963584 1448963616
id(b[0]), id(b[1]) = 1448963584 1448963616
a, b = [5, 3] [5, 3]
id(a), id(b) = 2379324095816 2379324095816
id(a[0]), id(a[1]) = 1448963680 1448963616
id(b[0]), id(b[1]) = 1448963680 1448963616

(5) リストでも「a =」なら再定義になる

もし、コード02の「b[0] = 5」が、「b = [5, 3]」だったらどうなるでしょうか。それは、下の図のようにリストオブジェクト[5, 3]が再定義されます。値の代入では「変数名 = 値」という書式で代入するとミュータブルであったとしても必ず再定義されます。左辺が変数名だけであることに注目してください。

出力03では、変数bの参照先のアドレス(id)が2379324095112から2379324187592に変更されていることがわかります。

もちろん、再定義ですのでミュータブル(変更可能体)あっても、値の変更が連動しません。

#コード03
a = [2, 3]
b = a   #参照渡し
print('a, b =', a, b)
print('id(a), id(b) =', id(a), id(b))
print('id(a[0]), id(a[1]) =', id(a[0]), id(a[1]))
print('id(b[0]), id(b[1]) =',id(b[0]), id(b[1]))
b = [5, 3]
print('a, b =', a, b)
print('id(a), id(b) =', id(a), id(b))
print('id(a[0]), id(a[1]) =', id(a[0]), id(a[1]))
print('id(b[0]), id(b[1]) =',id(b[0]), id(b[1]))
#出力03
a, b = [2, 3] [2, 3]
id(a), id(b) = 2379324095112 2379324095112
id(a[0]), id(a[1]) = 1448963584 1448963616
id(b[0]), id(b[1]) = 1448963584 1448963616
a, b = [2, 3] [5, 3]
id(a), id(b) = 2379324095112 2379324187592
id(a[0]), id(a[1]) = 1448963584 1448963616
id(b[0]), id(b[1]) = 1448963680 1448963616

(6) 余談

余談となりますが、再定義した場合でも出力03をよく見るとa[1]とb[1]のidが同じになっています。再定義なら全く別のidになるはずです。

実は変数aのリスト[2, 3]で整数オブジェクト3が生成済みなので、変数bの[5, 3]の3も同じ整数オブジェクト3を再利用しています。同じものを2度も作るなんて無駄ですよね。これは、Python内部のメモリー節約・スピードアップ術です。しかし、そこまで考えると複雑になりすぎます。再定義した場合には上の図のように全くあたらしいリストオブジェクトを生成したと考えたので問題ありません。

なぜなら、整数3はイミュータブル(変更不能体)だからです。イミュータブルは変更できないため、値を変更するときは元の整数オブジェクト3を放置します。つまり、同じ整数オブジェクト3を共有していたとしても、値の変更が他の変数に影響を与えることがないのです。
本当は変数には参照先のアドレス(id)しか入っていない

難しそうな話をしましたが、結局、「再定義でも本当はデータを共有していることがある。しかし、細かい内部のデータ処理の話なので無視し、全く新しい別の変数を定義したと考えてもよい」ということです。

4.ミュータブル(変更可能体)の浅いコピー

いよいよ、ミュータブル(変更可能体)の浅いコピーの説明です。ここまでの内容が理解できれば、それほど難しいことではありません。

要素が全て整数で構成されたリストについて浅いコピーを行った場合、リストの要素を変更しても他の変数は連動しません。ただし、リストの要素がミュータブルの場合には、リスト深い部分(リストの要素の要素)を変更すると連動してしまいます。

連動する部分と連動しない部分の区別がはっきりしない場合は以下の記事を参考にしてください。
「参照渡し」「浅いコピー」「深いコピー」まずは理屈抜きで覚えよう。

#コード04
import copy
a = [2, 3]
b = copy.copy(a)   #浅いコピー
print('a, b =', a, b)
print('id(a), id(b) =', id(a), id(b))
print('id(a[0]), id(a[1]) =', id(a[0]), id(a[1]))
print('id(b[0]), id(b[1]) =',id(b[0]), id(b[1]))
b[0] = 5
print('a, b =', a, b)
print('id(a), id(b) =', id(a), id(b))
print('id(a[0]), id(a[1]) =', id(a[0]), id(a[1]))
print('id(b[0]), id(b[1]) =',id(b[0]), id(b[1]))
#出力04
a, b = [2, 3] [2, 3]
id(a), id(b) = 2379324188296 2379324095816
id(a[0]), id(a[1]) = 1448963584 1448963616
id(b[0]), id(b[1]) = 1448963584 1448963616
a, b = [2, 3] [5, 3]
id(a), id(b) = 2379324188296 2379324095816
id(a[0]), id(a[1]) = 1448963584 1448963616
id(b[0]), id(b[1]) = 1448963680 1448963616

(1) 変数aの定義

コード04の3行目「a = [2, 3]」では、変数aにリスト[2, 3]を代入しています。今までの説明のとおり、下の図のようになっています。

(2) 変数bに浅いコピー

コード04の4行目では変数bに浅いコピーを行いました。参照渡しでは変数a、変数bは同じリストオブジェクトを参照していましたが、浅いコピーでは変数が参照するリストオブジェクトもコピーされています。

しかし、浅いコピーなのでリストオブジェクトが参照している整数オブジェクト2, 3は共有しています。このように、浅い部分(リストオブジェクト)だけコピーしているので浅いコピーと呼びます。英語ではshallow copy(シャローコピー)といいます。

(3) 変数bの要素の変更

ここまで読み進められたのであれば、変数bの要素を変更するとどうなるか予想できるのではないでしょうか。

要素の変更b[0] = 5では、整数オブジェクト2は放置され、m3番地に新しい整数オブジェクト5が再定義されます。変数bの要素の変更は変数aに影響を与えません。

このように、リストの要素がイミュータブル(変更不能体)の場合には浅いコピーをすることで、それぞれの変数は独立し互いに影響を与えないのです。

ただし、上の図で、整数オブジェクトの部分がリストだったらどうなるでしょうか。浅いコピーでは一番浅い部分のリストオブジェクトまでがコピーさるだけなので、それより深い部分にリストのようなミュータブル(変更可能体)があると、深い部分のリストの要素の変更は連動してしまうのです。

(4) 浅いコピーでは深い部分は連動する

コード05は4行目で浅いコピーが行われています。 浅いコピーでは一番上の要素の変更は連動しませんが、 11行目のb[0][0] = 5のような深い部分の変更は、変更の内容が連動してしまいます。

#コード05
import copy
a = [[1, 2], 3]
b = copy.copy(a)   #浅いコピー
print('a, b =', a, b)
print('id(a), id(b) =', id(a), id(b))
print('id(a[0]), id(a[1]) =', id(a[0]), id(a[1]))
print('id(b[0]), id(b[1]) =',id(b[0]), id(b[1]))
print('id(a[0][0]), id(a[0][1]) =', id(a[0][0]), id(a[0][1]))
print('id(b[0][0]), id(b[0][1]) =', id(b[0][0]), id(b[0][1]))
b[0][0] = 5
print('a, b =', a, b)
print('id(a), id(b) =', id(a), id(b))
print('id(a[0]), id(a[1]) =', id(a[0]), id(a[1]))
print('id(b[0]), id(b[1]) =',id(b[0]), id(b[1]))
print('id(a[0][0]), id(a[0][1]) =', id(a[0][0]), id(a[0][1]))
print('id(b[0][0]), id(b[0][1]) =', id(b[0][0]), id(b[0][1]))
#出力05
a, b = [[1, 2], 3] [[1, 2], 3]
id(a), id(b) = 2379324190472 2379324188296
id(a[0]), id(a[1]) = 2379324095560 1448963616
id(b[0]), id(b[1]) = 2379324095560 1448963616
id(a[0][0]), id(a[0][1]) = 1448963552 1448963584
id(b[0][0]), id(b[0][1]) = 1448963552 1448963584
a, b = [[5, 2], 3] [[5, 2], 3]
id(a), id(b) = 2379324190472 2379324188296
id(a[0]), id(a[1]) = 2379324095560 1448963616
id(b[0]), id(b[1]) = 2379324095560 1448963616
id(a[0][0]), id(a[0][1]) = 1448963680 1448963584
id(b[0][0]), id(b[0][1]) = 1448963680 1448963584

もう、詳しい説明は不要でしょう。図にすると以下のようになります。

なお、上の図のリストオブジェクト[5, 2]は、リストオブジェクトの中に整数オブジェクトの5や2が入っているような図になっていますが、実際にはリストオブジェクトから別のところにある整数オブジェクトの5や2を参照しています。

(5) リストオブジェクトごと入れ替える場合は注意

コード06は、結果だけ見ると深い部分の要素が1→5となっているように見えますが、7行目では浅い部分のb[0]をリストオブジェクトごと入れ替えています。したがって、bの変更が連動していません。

慣れないと、コード05の場合のようにb[0][0] = 5とした場合と混同してしまいますので注意しましょう。

#コード06
import copy
a = [[1, 2], 3]
b = copy.copy(a)   #浅いコピー
print('a', a, id(a), id(a[0]), id(a[0][0]))
print('b', b, id(b), id(b[0]), id(b[0][0]))
b[0] = [5, 2]
print('a', a, id(a), id(a[0]), id(a[0][0]))
print('b', b, id(b), id(b[0]), id(b[0][0]))
#出力06
a [[1, 2], 3] 2231994203400 2231994201416 1449946592
b [[1, 2], 3] 2231994203464 2231994201416 1449946592
a [[1, 2], 3] 2231994203400 2231994201416 1449946592
b [[5, 2], 3] 2231994203464 2231973865288 1449946720

5.ミュータブル(変更可能体)の深いコピー

さて、最後は深いコピー(deep copy)の説明です。深いコピーは全く独立した別の変数を生成するので、どんな変更を行っても連動しません。

なお、先ほども「3.ミュータブル(変更可能体)の参照渡し」の「(4) 余談」で記述しましたが、完全独立と言いながらidが同じになっている部分もあります。しかし、Python内部の細かい節約術ですので、無視して完全独立であると考えてください。

#コード07
import copy
a = [[1, 2], 3]
b = copy.deepcopy(a)   #深いコピー
print('a, b =', a, b)
print('id(a), id(b) =', id(a), id(b))
print('id(a[0]), id(a[1]) =', id(a[0]), id(a[1]))
print('id(b[0]), id(b[1]) =',id(b[0]), id(b[1]))
print('id(a[0][0]), id(a[0][1]) =', id(a[0][0]), id(a[0][1]))
print('id(b[0][0]), id(b[0][1]) =', id(b[0][0]), id(b[0][1]))
b[0][0] = 5
print('a, b =', a, b)
print('id(a), id(b) =', id(a), id(b))
print('id(a[0]), id(a[1]) =', id(a[0]), id(a[1]))
print('id(b[0]), id(b[1]) =',id(b[0]), id(b[1]))
print('id(a[0][0]), id(a[0][1]) =', id(a[0][0]), id(a[0][1]))
print('id(b[0][0]), id(b[0][1]) =', id(b[0][0]), id(b[0][1]))
#出力07
a, b = [[1, 2], 3] [[1, 2], 3]
id(a), id(b) = 2379324188296 2379324095560
id(a[0]), id(a[1]) = 2379321887304 1448963616
id(b[0]), id(b[1]) = 2379324190472 1448963616
id(a[0][0]), id(a[0][1]) = 1448963552 1448963584
id(b[0][0]), id(b[0][1]) = 1448963552 1448963584
a, b = [[1, 2], 3] [[5, 2], 3]
id(a), id(b) = 2379324188296 2379324095560
id(a[0]), id(a[1]) = 2379321887304 1448963616
id(b[0]), id(b[1]) = 2379324190472 1448963616
id(a[0][0]), id(a[0][1]) = 1448963552 1448963584
id(b[0][0]), id(b[0][1]) = 1448963680 1448963584

(1) 変数aの定義

完全独立なので、図にするほどのこともないですが、同様に図示します。まずは、コード07の3行目で変数aを生成します。

(2) 変数bに深いコピー

深いコピーでは、完全に独立した変数を生成します。

(3) 変数bの要素の変更

変数bの深い部分の要素を変更しても、全く別物なので他の変数に影響は与えません。

以上で、「参照渡し」「浅いコピー」「深いコピー」の図による説明は終了です。変数には参照先アドレスのみが代入され、変数に代入された整数などのデータは独立したオブジェクトとして別の場所に保管されていることは覚えておきましょう。

6.NumPyはデータ管理の方法が全く違う

タプルやディクショナリなどの組み込み関数は、データの管理の方法が同様ですが、外部ライブラリであるNumPyはデータ管理の方法が全く異なります。 NumPy などの外部ライブラリーは、その仕様をそれぞれ確認する必要があるので注意してください。

私が実際に購入した教材のご紹介

以下、私が実際に購入したPythonの教材をまとめてみました。 Pythonを学習する上で、少しでもお役に立つことができればうれしいです。

Python♪私が購入したPythonの書籍のレビュー
UdemyのPythonの動画講座を書籍を買う感覚で購入してみた

その他

Twitterへのリンクです。SNSもはじめました♪

液晶ペンタブレットを買いました
 (1) モバイルディスプレイを買うつもりだったのに激安ペンタブレット購入

以下、私が光回線を導入した時の記事一覧です。
 (1) 2020年「光回線は値段で選ぶ」では後悔する ←宅内工事の状況も説明しています。
 (2) NURO光の開通までWiFiルーターを格安レンタルできる
 (3) NURO光の屋外工事の状況をご紹介。その日に開通!
 (4) 光回線開通!実測するとNURO光はやっぱり速かった
 (5) ネット上のNURO光紹介特典は個人情報がもれないの?