Python♪「参照渡し後の要素の変更」と「参照渡し後の再定義」の違い

 関数の引数や、関数内からのグローバル変数の参照などは「参照渡し」の考え方が用いられています。この様に参照渡しは様々なところで使われているため、参照渡しが理解できていないと思わぬミスをしてしまいます。この記事では、「参照渡し」だけに注目し、変更不能体(イミュータブル)、リスト、ディクショナリ、集合(set)、NumPy配列の「参照渡し」について整理します。

参考記事: まずは、理屈抜きで覚えよう。「参照渡し」「浅いコピー」「深いコピー」

 参照渡し(b = a)は、aの完全な複製をbに渡すわけではありません。データを保管しているアドレスをaからbに渡し、同じ場所に記憶されたデータを共有しているだけです。したがって、参照渡し後に片方の変数を変更すると、もう片方の変数も連動して同じように変更する場合があります。ここで「場合がある」といったのは、連動して変更されない場合もあるからです。「連動する」か「連動しない」かは、「参照渡し」後の変数の変更の仕方によります。

 結果から言うと、「参照渡し」後に、要素の変更を行うと、もう片方の変数も連動します。一方、「参照渡し」後に、「変数の再定義」を行うと、もう片方の変数は連動しません。この違いは、変更する式の左辺に注目します。左辺に注目し「変数名 = 」の形になっていれば、それは「変数の再定義」です。「変数の再定義」は、今の場所を消して上書きするのではなく、現在の記憶場所を放置し、別の記憶場所に定義し直します。

(1) 参照渡し後の変数の再定義→ もう片方の変数は連動しない。
 例:a = 2, a = [2], a = (1, 2) ※左辺が「変数名 = 」となっている。

(2) 参照渡し後の要素の変更 → もう片方の変数も連動する。
 例:a[0] = 2, a[1][3] = 5 ※左辺が「変数の要素を示す形 = 」となっている。

それでは、以下、図を用いて違いを説明したいと思います。

参照渡し後のリストの要素の変更

 この例では、参照渡しb = aの後、a[0] = 5としています。参照渡し b = aでは、変数aの複製を変数bとして作っているのではなく、図のように、アドレスi番地に保管したデータ[3]をaとbで共有します。b = aでは、参照先のアドレス(i番地)をaからbに渡しただけなのです。
 次にa[0] = 5の左辺に注目すると、「変数の要素を示す形 = 」となっており、これは要素の変更です。要素の変更では、[3]の箱の中身を5から3に入れ替えるので、aを変更した場合、bも連動して内容が変更されます。要素の変更では、参照先のアドレスはi番地のまま変わりません。

参照渡し後の数値の再定義

 今度は、参照渡しb = aの後、a = 5としています。参照渡し b = aは、先ほどと同様にアドレスi番地に保管したデータ[3]をaとbで共有します。
 次にa = 5の左辺に注目すると、「変数 = 」となっており、これは変数の再定義です。変数の再定義では、変数aは、それまでの記憶を放置し、全く新しいアドレスk番地に新しい変数aを再定義します。この時点で、変数aと変数bは、なんの関わりもないそれぞれ独立した変数になってしまいます。つまり、a 変更がbに影響をあたえることはありません。
 変更不能体(イミュータブル)には、タプル、数値型、文字列、ブール型、フローズンセットがありますが、そもそも、変更不能体は変更できないので、そのため再定義するしか方法がなく、他方に影響を与えることはできないのです。

参照渡し後のリストの再定義

 下図は、参照渡しb = aの後、a = [5]としています。リストは変更可能体(ミュータブル)です。ここで、参照渡し後のa = [5]の左辺に注目すると、「変数名 = 」となっています。つまり、これは「変数の再定義」なのです。変数aは、それまでの記憶は放置し、全く新しいアドレスk番地に新しい変数aを再定義します。したがって、変数aと変数bは、なんの関わりもないそれぞれ独立した変数なのです。aの再定義がbに影響をあたえることはありません。
 このように、単純に「変更可能体(ミュータブル)は、他方の変数も連動する」と覚えてしまうと間違えてしまいます。変更可能体であっても、再定義すると独立した変数になります。

これは「再定義」それとも「要素の変更」

 コード01を見てください。「再定義」なのか、それとも「要素の変更」なのか、注意しないと間違えそうなものを集めてみました。

 さて、おさらいですが、「再定義」は、データを保管するメモリーのアドレスが変化します。一方、「要素の変更」ではデータを保管するメモリーのアドレスは変化しません。
 そこでid()関数を使って、「再定義」なのかか「要素の変更」なのかを調べてみます。id()関数は、その変数の参照先のアドレス(データが実際に保管されているメモリーのアドレス)を調べることができるので、「再定義」か「要素の変更」なのかを確認することができます。参照先のアドレス(id)が変わっていれば、「再定義」です。

 まず、数値は変更不能体(イミュータブル)なので、どんな変更であろうと再定義です。出力01の2~4行目をみると、参照先のアドレスが497774048 → 497774112 → 497774208と変化しており、内容の変更を行う度に再定義されていることがわかります。

 しかし、難しいのはリストです。a = a + [2] は、左辺が「変数名 =」の形になっているので、再定義だとわかりますが、a += [6], a.append(7)は、参照先アドレス(id)が変化していません。つまり、再定義ではなく「要素の変更」なのです。
 特にa = a + [5]は「再定義」で、a += [6]は「要素の変更」なので、注意が必要です。また、a += [6]は「要素の変更」で、a += 3は「再定義」なのもまぎらわしいです。
 この様に、「再定義」なのか「要素の変更」なのかを個別に覚えなければならないものもありますので、注意しましょう。

#コード01

#数値の変更
a = 1
print(id(a), a, '← a = 1')
a = a + 2 #再定義
print(id(a), a, '← a = a + 2')
a += 3 #再定義。
print(id(a), a, '← a += 3')
#リストの変更
a = [4]
print(id(a), a, '← a = [4]')
a = a + [5] #再定義
print(id(a), a, '← a = a + [5]')
a += [6] #要素の変更
print(id(a), a, '← a += [6]')
a.append(7) #要素の変更
print(id(a), a, '← a.append(7)')
#出力01
497774048 1 ← a = 1
497774112 3 ← a = a + 2
497774208 6 ← a += 3
197676872 [4] ← a = [4]
197547336 [4, 5] ← a = a + [5]
197547336 [4, 5, 6] ← a += [6]
197547336 [4, 5, 6, 7] ← a.append(7)

まとめ

 参照渡し後の変数の変更が他方の変数に影響をあたえるかどうかをまとめます。いろいろ、説明しましたが、まとめると単純ですね。この記事の結果は、必ず理解してください。色々なところで役に立つはずです。

(1) 変更不能体(イミュータブル)

 「参照渡し」後の、変更不能体の変更は要素の変更ができないため、「変数の再定義」しかなく、片方の変数の変更が他方の変数に影響を与えることはありません。なお、「変更不能体→変更不能体」だけではなく、「変更不能体→変更可能体」「変更可能体→変更不能体」の場合も「変数の再定義」以外には変更の手段がありません。片方の変数の変更が、他方の変数に影響を与えることはありません。

(2) リスト、ディクショナリ、集合(set)、NumPy配列

(a) 参照渡し後の変数の再定義 → もう片方の変数は連動しない。
 例:a = 2, a = [2], a = (1, 2) ※左辺が「変数名 = 」となっている。

(b) 参照渡し後の要素の変更 → もう片方の変数も連動する。
 例:a[0] = 2, a[1][3] = 5 ※左辺が「変数の要素を示す形 = 」となっている。

(3)「再定義」か「要素の変更」か覚えなければならない例

(a)再定義(aの参照先アドレスが変わるもの)

① a = [1]; a = a + [2] →左辺が「変数 =」になっているので「再定義」
② a = 1; a = a + 2 →左辺が「変数 =」になっているので「再定義」
③ a = 1; a += 2 →変更不能体の変更なので「再定義」

(b)要素の変更(aの参照先アドレスが変わらないもの)

① a = [1]; a += [2] →これが「要素の変更」になることは覚えよう。
② a = [1]; a.append(2) →これが「要素の変更」になることは覚えよう。

(4)参考記事

 以下、この記事を読めば理解が進む記事を紹介します。Pythonを理解する上で重要だと思いますので、参考記事にも是非、目を通してください。

(a) 関数に引数で渡された変数の変更が、関数の外側に与える影響。
(b) 関数内からグローバル変数を変更したときに関数の外側に与える影響
(c) 引数のディフォルト値が変化する場合と変化しない場合の違い

2.ブログリンク