はじめに
Pythonを学び始めると、変数について「値を入れておく箱」と説明されることがあります。
この説明は入門段階では分かりやすいのですが、Pythonの動きを正確に理解しようとすると、少し困る場面が出てきます。
たとえば、次のコードです。
x = [1, 2, 3]
y = x
y.append(4)
print(x)
実行結果は次のようになります。
[1, 2, 3, 4]
y を変更しただけなのに、なぜ x まで変わったのでしょうか。この疑問を解く鍵になるのが、「変数」と「オブジェクト」を分けて考えることです。
この記事では、Pythonの変数を「箱」ではなく「オブジェクトへの参照」として捉えながら、代入、再代入、ミュータブル、コピー、そしてメモリ管理まで順番に見ていきます。
この記事でわかること
この記事では、次の内容の理解を目指します。
- Pythonの変数は「箱」ではなく、オブジェクトを参照する名前である
- Pythonにおける「オブジェクト」とは何か
==とisの違い- 再代入では何が起きているのか
- ミュータブルとイミュータブルの違い
- 代入とコピーの違い
- CPythonにおける参照カウントとガーベージコレクションの基本
1. Pythonの変数は「箱」ではない
まず、次のコードを考えてみます。
x = 10
「変数は箱」と考えると、x という箱の中に 10 が入ったように見えます。
しかしPythonでは、x という名前が整数オブジェクト 10 を参照していると考えるほうが、実際の動作を理解しやすくなります。
イメージすると、次のような関係です。

ここで重要なのは、変数名とオブジェクトを分けて考えることです。x と 10 が一体になっているわけではありません。x は名前であり、その名前が 10 というオブジェクトを指しています。
Pythonの変数は「値を入れる箱」ではなく、「オブジェクトを参照する名前」と考えると理解しやすくなります。
2. Pythonでは、ほとんどすべてがオブジェクト
Pythonでは、整数、文字列、リスト、関数など、さまざまなものがオブジェクトとして扱われます。たとえば、
x = 10
と書いたとき、10 は整数オブジェクトです。
オブジェクトを理解するときには、特に次の3つを意識すると分かりやすくなります。
- 型(type)
- 値(value)
- 識別性(identity)
2-1. 型(type)
型は、そのオブジェクトがどのような種類のデータなのかを表します。type() を使うと確認できます。
x = 10
print(type(x))
実行結果は次のようになります。
<class 'int'>
この場合、x が参照しているオブジェクトの型は int です。型によって、そのオブジェクトに対してどのような操作ができるのかが決まります。
2-2. 値(value)
値は、そのオブジェクトが持っているデータです。たとえば、
x = 10
name = "Python"
numbers = [1, 2, 3]
なら、それぞれの値は 10、"Python"、[1, 2, 3] です。
2-3. 識別性(identity)
もう一つ重要なのが、オブジェクトの「識別性(identity)」です。Pythonでは、値が同じかどうかとは別に、「同じオブジェクトなのか」を区別できます。
id() 関数を使うと、そのオブジェクトを識別するための値を確認できます。
x = 10
print(id(x))
id() が返す値は、そのオブジェクトが存在している間、一意で変化しません。
ただし、id() を単純に「メモリアドレス」と考えるのは正確ではありません。CPythonではメモリアドレスに対応する値になることがありますが、Python言語として保証されているのは、オブジェクトの生存中に一意であることです。
「同じ値を持っていること」と「同じオブジェクトであること」は別の話です。
2-4. == と is の違い
オブジェクトの「値」と「同一性」の違いが分かると、== と is の違いも理解しやすくなります。次のコードを考えてみましょう。
x = [1, 2, 3]
y = x
z = [1, 2, 3]
このとき、x と y は同じオブジェクトを参照しています。一方、z は値こそ同じですが、別のオブジェクトを参照しています。
図にすると、次のような関係です。

ここで、== と is を使って比較してみます。
print(x == z)
print(x is z)
実行結果は次のようになります。
True
False
x == z は True です。2つのリストが持つ値が等しいからです。一方、x is z は False です。値は同じでも、x と z が参照しているのは別のオブジェクトだからです。
==:値が等しいか
is:同じオブジェクトか
なお、== の具体的な比較方法はオブジェクトの型によって定義できます。そのため、厳密には「常に中身を単純比較する演算子」というわけではありません。
3. y = x ではオブジェクトはコピーされない
では、次の代入をもう少し詳しく見てみましょう。
x = 10
y = x
このコードを見ると、x に入っている 10 をコピーして y に入れたように感じるかもしれません。しかし、新しい整数オブジェクトをコピーしているわけではありません。
y = x によって、y も x が参照しているオブジェクトを参照するようになります。つまり、次のような関係です。
x ─┐
├─→ 10
y ─┘
この「複数の名前が同じオブジェクトを参照できる」という仕組みが、Pythonの変数を理解するうえで重要になります。
4. 再代入では「参照先」が変わる
では、同じ変数に別の値を代入するとどうなるのでしょうか。
x = 10
x = 20
箱のイメージでは、「x の中身を10から20に入れ替えた」と考えたくなります。しかし、オブジェクトと参照で考えると少し違います。
整数オブジェクト 10 が 20 に変化したのではなく、x の参照先が 10 から 20 に変わったと考えます。

このように、再代入で変化しているのは x が指している先です。この考え方は、あとで出てくる「ミュータブル」と「イミュータブル」の違いを理解するときにも重要になります。
再代入ではオブジェクトそのものが変化するのではなく、変数名が参照するオブジェクトが変わります。
5. ミュータブルとイミュータブル
ここで重要になるのが、「ミュータブル」と「イミュータブル」の違いです。
イミュータブルなオブジェクトは、一度作られるとその内容を変更できません。一方、ミュータブルなオブジェクトは、同じオブジェクトのまま内容を変更できます。
代表的なイミュータブルな型には、次のようなものがあります。
intfloatstrtuple
一方、代表的なミュータブルな型には、次のようなものがあります。
listdictset
整数とリストを比べると、この違いが分かりやすくなります。

たとえば、整数について、
x = 10
x = 20
とした場合、10 という整数オブジェクト自体が 20 に変化したわけではありません。x が別の整数オブジェクト 20 を参照するようになっただけです。
一方、リストでは次のような操作ができます。
numbers = [1, 2, 3]
numbers.append(4)
この場合、numbers が別のリストを参照するようになったわけではありません。同じリストオブジェクトの内容が [1, 2, 3] から [1, 2, 3, 4] へ変化しています。
再代入は「参照先を変える操作」、ミュータブルなオブジェクトへの変更は「参照先のオブジェクト自体を変える操作」です。

6. 「なぜ x まで変わるの?」問題
ここまでの内容を使って、冒頭で紹介したコードに戻ってみましょう。
x = [1, 2, 3]
y = x
y.append(4)
print(x)
実行結果は、
[1, 2, 3, 4]
でした。
まず、x = [1, 2, 3] によってリストオブジェクトが作られ、x がそのオブジェクトを参照します。続いて、y = x とすると、リストがコピーされるわけではありません。y も同じリストオブジェクトを参照するようになります。
つまり、x と y は別々のリストを持っているのではなく、同じリストに付けられた2つの名前のような状態です。
そのため、y.append(4) とすると、y が参照しているリストオブジェクトそのものが変更されます。x も同じオブジェクトを参照しているため、print(x) の結果も [1, 2, 3, 4] となるわけです。
「変数は箱ではなく、オブジェクトを参照する名前」と考えると、なぜ y を変更したときに x からも変更が見えるのかを説明できます。
7. 「代入」と「コピー」は違う
では、x と y を別々のリストとして扱いたい場合はどうすればよいのでしょうか。ここで重要になるのが、「代入」と「コピー」の違いです。
y = x
とした場合、リストそのものがコピーされるわけではありません。x と y が同じオブジェクトを参照します。
一方、
y = x.copy()
とすると、新しいリストオブジェクトが作られます。この2つの違いを図で比べてみましょう。

y = x では、x と y が同じリストオブジェクトを参照します。そのため、一方の名前を使ってリストを変更すると、もう一方から見ても変更されています。
一方、次のようにコピーした場合はどうでしょうか。
x = [1, 2, 3]
y = x.copy()
y.append(4)
print(x)
print(y)
実行結果は次のようになります。
[1, 2, 3]
[1, 2, 3, 4]
x と y は別のリストオブジェクトを参照しているため、y を変更しても x には影響しません。
代入はオブジェクトをコピーする操作ではありません。y = x では、x と y が同じオブジェクトを参照します。
copy() は「浅いコピー」
ただし、ここで一つ注意があります。リストの copy() で作られるのは、浅いコピー(shallow copy)です。
たとえば、次のような入れ子のリストを考えてみます。
x = [[1, 2], [3, 4]]
y = x.copy()
この場合、外側のリストは別のオブジェクトになりますが、その中に入っている内側のリストまですべてコピーされるわけではありません。
そのため、
y[0].append(5)
print(x)
とすると、x 側から見える内側のリストにも変更が現れます。
[[1, 2, 5], [3, 4]]
入れ子になったオブジェクトまで含めて独立したコピーが必要な場合は、copy.deepcopy() などを検討する必要があります。
8. オブジェクトはいつ消えるのか?
ここまで、変数名がオブジェクトを参照する仕組みを見てきました。では、どの変数名からも参照されなくなったオブジェクトはどうなるのでしょうか。
たとえば、最初は x と y が同じリストを参照しているとします。
x = [1, 2, 3]
y = x
その後、次のように参照を外していきます。
x = None
y = None
この流れを図で追ってみましょう。

最初は x と y の2つがリストを参照しています。x = None としても y から参照されているため、リストはまだ利用できます。
しかし、さらに y = None とすると、この例では x と y のどちらからも、そのリストを参照しなくなります。
ここで注意したいのは、図にある「参照:2 → 1 → 0」は、説明対象である x と y からの参照数を模式的に表したものだということです。CPython内部の実際の参照カウント値そのものを表しているわけではありません。
変数名とオブジェクトは別物なので、変数名の参照先が変わっても、ほかの場所から参照されているオブジェクトはそのまま存在できます。
9. CPythonの参照カウントとガーベージコレクション
ここから先は、Pythonという言語そのものの仕組みというより、Python処理系がメモリをどのように管理するかという話です。
Pythonにはさまざまな実装がありますが、代表的な実装であるCPythonでは、オブジェクトへの参照の数を管理する仕組みとして、参照カウントが使われています。簡単にいえば、あるオブジェクトが「何か所から参照されているか」を管理する仕組みです。
オブジェクトへの参照がなくなると、そのオブジェクトは不要になります。CPythonでは通常、参照カウントが0になったオブジェクトは解放されます。
ただし、参照カウントだけでは処理できないケースもあります。たとえば、オブジェクト同士がお互いを参照している「循環参照」です。
a = []
b = []
a.append(b)
b.append(a)
このようなケースでは、変数 a や b から参照しなくなっても、リスト同士がお互いを参照している状態が残ることがあります。
そこでCPythonでは、参照カウントに加えて、循環参照を検出するためのガーベージコレクションも使われています。
「変数がオブジェクトを参照する」という考え方はPythonのプログラムを理解するためのモデルです。一方、参照カウントや循環GCは、CPythonという実装のメモリ管理の仕組みです。
この2つを分けて考えると、「Pythonの変数の仕組み」と「CPython内部のメモリ管理」が混ざりにくくなります。
10. x = 10 をもう一度考えてみる
最後に、シンプルなコードをオブジェクトと参照の視点から見直してみましょう。
x = 10
y = x + 2
print(y)
まず、x = 10 によって、x が整数オブジェクト 10 を参照します。
次に、y = x + 2 が実行されます。ここでは、x が参照している 10 と 2 を使って加算が行われ、その結果として 12 という整数オブジェクトが得られます。そして、y がその 12 を参照します。
つまり、概念的には、
x ─→ 10
y ─→ 12
という状態です。
x の中に入っていた値を取り出して y の箱に移したわけではありません。また、10 という整数オブジェクト自体が 12 に変化したわけでもありません。
Pythonの変数そのものが値を持っているのではなく、変数名を通してオブジェクトを参照していると考えるのがポイントです。
このように考えると、Pythonの代入や再代入、ミュータブルなオブジェクトの変更が、すべて同じ「名前とオブジェクト」という考え方でつながってきます。
11. Pythonの変数を理解すると、いろいろなことがつながる
Pythonの変数を単純な「箱」として考えていると、
y = xでなぜ同じリストを参照するのか- なぜリストを変更すると別の変数からも変更が見えるのか
- なぜ
intは変更できないのか - なぜ
copy()が必要なのか - オブジェクトはいつメモリから解放されるのか
といった疑問が、それぞれ別々の話に見えてしまいます。
しかし、「Pythonの変数はオブジェクトを参照する名前である」という考え方を軸にすると、これらが一つにつながります。
Pythonを少し深く理解したいなら、この「オブジェクトと参照」という考え方はぜひ押さえておきたいポイントです。
まとめ
Pythonの変数について、今回のポイントをまとめます。
- Pythonでは、変数名とオブジェクトは別のものとして考える
- 変数名はオブジェクトを参照する
- 複数の変数名が同じオブジェクトを参照できる
==は値が等しいか、isは同じオブジェクトかを調べる- 再代入では、変数名の参照先が変わる
- ミュータブルなオブジェクトは、同じオブジェクトのまま内容を変更できる
y = xはオブジェクトのコピーではないcopy()は新しいリストを作るが、浅いコピーになる- CPythonでは参照カウントやガーベージコレクションによってオブジェクトが管理される
Pythonの変数を理解するポイントは、「変数の中に値が入っている」と考えるのではなく、「名前がオブジェクトを参照している」と考えることです。
この考え方が身につくと、
x = [1, 2, 3]
y = x
で、なぜ x と y が同じリストに影響するのかも自然に理解できます。
そして、この「オブジェクトと参照」という考え方は、リストや辞書だけでなく、関数の引数、クラスのインスタンス、コピー、メモリ管理など、Pythonのさまざまな仕組みを理解する土台になります。
Pythonを学ぶとき、「変数は箱」というイメージで分かりにくくなったら、ぜひ「名前 → オブジェクト」という関係に戻って考えてみてください。
関連記事
次回の記事:Pythonのミュータブル・イミュータブルとは?|型ごとの違いを整理

関連書籍
Pythonの文法だけでなく、オブジェクトやデータモデルなど、Pythonの仕組みをもう少し深く理解したい人には『入門 Python 3』もおすすめです。



コメント