変数と変更
代入は名前をオブジェクトへ結び、変更は既存のオブジェクトを編集します。二つの名前が同じ可変オブジェクトを指せば、どちらからも変更が見えます。一方の名前を再代入しても、他方の束縛は移りません。
名前を荷札、オブジェクトをスーツケースと考えます。同じケースに二枚の札を付けた場合、中身の変更は両方の持ち主に見えます。一枚の札を別のケースへ付け替えても、もう一枚は残ります。関数へ渡したリストの変更が呼び出し元にも見えるのは、同じオブジェクトを共有するためです。
変数と変更の仕組み
Python の変数はオブジェクトに束縛された名前です。a = [1, 2] はリストを作り、a を結びます。名前がリストを格納するのではなく、アクセスする経路になります。
b = a はコピーせず、b を同じオブジェクトへ結びます。a と b は別名で、名前は二つ、リストは一つです。どちらから変更しても、一つの共有オブジェクトが変わります。
b.append(3) ではリストが [1, 2] から [1, 2, 3] へ変わり、束縛の矢印は残ります。そのため a からも新しい内容を読めます。
b = [9] は別のリストを作り、b だけを付け替えます。元のリストは変わらず、a はそこに残ります。
- オブジェクトの変更:内容が変わり、束縛は残ります。
- 代入による束縛:名前の参照先が変わり、再束縛そのものは既存オブジェクトを変えません。
構文だけでは判断できない場合もあります。リストは可変ですが、整数は不変です。整数例の b += 1 は原位置の更新に見えても、整数オブジェクト 7 を変更できません。値 8 を表す別の整数オブジェクトを得て、b を再束縛します。a は 7 のままです。
ステップごとの解説
既定は可変リストの例です。
- 最初の行で
[1, 2]を持つL1を作ります。割り当ての段階では名前はまだありません。 - 同じ行で
aをL1に束縛し、一つの名前と一つのオブジェクトを結びます。 b = aで二つ目の束縛を加えます。二つ目のリストは作らず、状態はa -> L1とb -> L1です。b.append(3)がL1を[1, 2, 3]に変更します。両方の矢印はL1に残ります。[9]の評価で別のリストL2を作ります。代入はまだ完了せず、両名ともL1を指しています。- 代入が
bをL1からL2へ移します。aはL1に残り、内容は[1, 2, 3]のままです。 - 最後の表示は両束縛から読み、
[1, 2, 3] [9] Falseを出力します。 - 終了画面には両オブジェクトが残り、
a -> L1、b -> L2となります。
オブジェクトと矢印を別々に追います。変更では内容だけが変わり、再束縛では一本の矢印だけが移ります。この二つを一つの操作として考えないでください。
次に不変な整数の例へ切り替えます。同じ別名の設定から始まりますが、b += 1 は値 8 を得て b を付け替えます。7 を表すオブジェクトは編集できないためです。
+= が再束縛になる理由
+= は原位置の変更を約束しません。組み込みリストなら同じリストを拡張し、組み込み整数なら結果を計算して名前を再束縛します。同じ演算子でも、型が状態の変化を決めます。
等しい値を持つ二つのオブジェクトが、同じオブジェクトとは限りません。等価性は値を、同一性は参照先そのものを比べます。
本例の a is b は、最終的な束縛図を見せるためです。b の再束縛後は別のオブジェクトへ着くため False になります。通常の値比較には == を使います。ランタイムが一部の不変オブジェクトを再利用することがあるので、数値や文字列の比較に is を使うと不具合になります。
関数に可変オブジェクトを渡す場合も別名参照が生じます。リスト引数へ追加すると、同じオブジェクトを参照する呼び出し元にも見えます。局所パラメーターを再代入するだけなら、呼び出し元の束縛は変わりません。変わったのが束縛かオブジェクトかを明示すると、この挙動を説明できます。
境界ケース
- 二つの名前で一つの可変オブジェクトを共有でき、どちらからの変更も他方に見えます。
- 最後の到達可能な名前を再束縛すると、オブジェクトが到達不能になり、その後回収されることがあります。回収と代入は別の処理です。
- 整数、文字列、タプルなどの不変オブジェクトは原位置で変えられず、更新に見える式が新しい値の計算と再束縛になることがあります。
- 不変なタプルも可変リストを含められます。タプルのスロットは固定でも、その参照先のリストは変えられます。
a == bは値、a is bはオブジェクトの同一性を比べます。
よくある間違い
- 各変数を値を専有する箱として描く。リストを共有する場合に破綻するため、名前からオブジェクトへの束縛で表します。
b = aが可変オブジェクトをコピーすると思う。独立した状態が必要なら明示的にコピーします。+=が必ず元のオブジェクトを変えると思う。型と操作の実装に依存します。- 普通の値を
isで比較する。Noneなどの番兵の同一性には適しますが、通常の値には==を使います。 - 関数が「変数を変えた」とだけ説明する。束縛が動いたのか、内容が変わったのかを区別します。
このレッスンでの簡略化
L1、L2、I7、I8 は説明用ラベルで、メモリアドレスではありません。実際のランタイムはメモリ割り当て、整数の再利用、参照カウントなどの回収機構、オブジェクト配置も管理します。これらは保存や寿命に影響しても、名前はオブジェクトを参照し、代入は束縛を変え、変更は可変オブジェクトに作用するという規則を変えません。