Pythonの変数は「箱」ではない?|オブジェクトと参照を理解する

ソフトウェア
スポンサーリンク

はじめに

Pythonを勉強していると、変数についてこんなふうに考えることがあると思います。

x = 10

x という箱を用意して、そこに 10 を入れる」

入門書では、このようなイメージで説明されることもあります。

もちろん、最初にPythonを学ぶうえでは、この理解でも大きな問題はありません。

しかし、Pythonをもう少し深く理解しようとすると、

x = 10
y = x

や、

x = [1, 2, 3]
y = x

y.append(4)

のようなコードで、

「なぜこういう動きになるの?」

と疑問に感じることがあります。

実はPythonの変数は、厳密には「値を入れておく箱」ではありません。

変数は、オブジェクトを参照するための名前です。

この「オブジェクト」と「参照」を理解すると、

  • ミュータブル・イミュータブル
  • リストのコピー
  • 関数の引数

など、Pythonで「なぜこうなるの?」と感じやすい現象が、一つの考え方で説明できるようになります。

この記事では、Pythonの変数がどのような仕組みになっているのかを、できるだけ図を使いながら整理していきます。

この記事でわかること

この記事では、次の内容の理解を目指します。

  • Pythonの変数は「箱」ではなく、オブジェクトを参照する名前である
  • Pythonにおける「オブジェクト」とは何か
  • x = 10 のとき、どのようにオブジェクトが参照されるのか
  • x = y としたとき、なぜ同じオブジェクトを参照するのか
  • ミュータブルとイミュータブルの違い
  • 代入とコピーの違い
  • CPythonにおける参照カウントとガーベージコレクションの基本

1. Pythonの変数は「箱」ではない

まずは、もっとも基本的なコードから見てみましょう。

x = 10

初心者向けには、

「変数 x10 を代入する」

と説明されます。

この説明はもちろん間違いではありません。

ただし、Pythonの仕組みをより正確に理解するなら、

10 というオブジェクトに、x という名前を付けて参照している

と考えるほうが適切です。

次のようなイメージです。

変数はオブジェクトを参照する名前

※図のIDは説明のための記号です。実際に表示されるIDの値ではありません。

x そのものが 10 という値を内部に持っているというより、

x という名前から 10 というオブジェクトを参照している

と考えます。

この違いは、後で説明するミュータブル・イミュータブルを理解するときに重要になります。

2. そもそもPythonの「オブジェクト」とは?

では、「オブジェクト」とはいったい何なのでしょうか。

Pythonでは、整数や文字列、リスト、関数など、基本的にすべてがオブジェクトとして扱われます。

例えば、

x = 10

なら、10 は整数オブジェクトです。

name = "Python"

なら、"Python" は文字列オブジェクトです。

numbers = [1, 2, 3]

なら、[1, 2, 3] はリストオブジェクトです。

オブジェクトを理解するときには、特に次の3つを意識するとわかりやすくなります。

  • 型(type)
  • 値(value)
  • 識別性(identity)

Pythonでは、オブジェクトは「型(type)」「値(value)」「識別性(identity)」という3つの特徴を持つものとして考えることができます。

2-1. 型(type)

型は、そのオブジェクトがどのような種類のデータなのかを表します。

例えば、

x = 10

なら、x が参照しているオブジェクトの型は int です。

print(type(x))

実行すると、

<class 'int'>

と表示されます。

型によって、そのオブジェクトに対してどのような操作ができるのかが決まります。

例えば整数なら、

x + 10
x * 2
x / 3

といった計算ができます。

一方、リストなら、

numbers.append(4)

のような操作ができます。

つまり型は、

「このオブジェクトをどのように扱えるのか」

を決めるものだと考えるとわかりやすいでしょう。

2-2. 値(value)

値は、そのオブジェクトが持っているデータです。

例えば、

x = 10

なら、値は 10 です。

name = "Python"

なら、値は "Python" です。

numbers = [1, 2, 3]

なら、値は [1, 2, 3] です。

2-3. 識別性(identity)

もう一つ重要なのが、オブジェクトの「識別性(identity)」です。

Pythonでは、オブジェクトが同じオブジェクトなのかどうかを区別することができます。

id() 関数を使うと、そのオブジェクトを識別するためのIDを確認できます。

x = 10

print(id(x))

ここで表示される数値が、そのオブジェクトのIDです。

ただし、表示される具体的な数値そのものを覚える必要はありません。

重要なのは、

「同じ値を持っていること(valueが同じ)」と「同じオブジェクトであること(identityが同じ)」は別の話

ということです。

この「値(value)」と「オブジェクトの同一性」の違いは、Pythonを理解するうえで重要なポイントになります。

2-4. == と is の違い

ここまで読んで、

「同じ値」と「同じオブジェクト」が別なら、Pythonではどうやって区別するの?

と思うかもしれません。

そこで覚えておきたいのが、==is の違いです。

例えば、

x = [1, 2, 3]
y = x
z = [1, 2, 3]

とします。

このとき、

print(x == y)
print(x == z)

print(x is y)
print(x is z)

を実行すると、

True
True
True
False

となります。

== は、基本的に「値が等しいか」を比較します。

一方、is は「同じオブジェクトかどうか」を比較します。

つまり、

==:値が等しいか
is:同じオブジェクトか

という違いがあります。

図にすると、次のようなイメージです。

== と is

xy は同じオブジェクトを参照しているため、

x == y  # True
x is y  # True

となります。

一方、xz は値は同じですが、別々のオブジェクトなので、

x == z  # True
x is z  # False

となります。

このように、「同じ値なのか」「同じオブジェクトなのか」を区別して考えることが、Pythonのオブジェクトを理解するうえで重要です。

3. x = 10 では何が起きている?

ここまでを踏まえて、もう一度、

x = 10

を考えてみましょう。

x という箱に 10 を入れる」と考える代わりに、

x = 10

と考えます。

x はオブジェクトそのものではありません。

オブジェクトを参照するための名前です。

この考え方を理解すると、次のコードもわかりやすくなります。

x = 10
y = x

このとき、

x と y が同じオブジェクトを参照

となります。

y = x によって、10 というオブジェクトを新しくコピーしたわけではありません。

y も同じオブジェクトを参照するようになった

と考えるのがポイントです。

4. 変数に新しい値を代入するとどうなる?

では、

x = 10
x = 20

では何が起きるのでしょうか。

x の中身が 10 から 20 に書き換わった」

と考えることもできます。

しかし、Pythonのオブジェクトと参照という考え方では、

x = 10の状態

だったものが、

その後

x = 20の状態

になったと考えます。

つまり、

10 という整数オブジェクトを 20 に変更したわけではありません。

x という名前が、参照するオブジェクトを変更したのです。

この違いが、後で出てくる「イミュータブル」という概念につながります。

5. ミュータブルとイミュータブル

Pythonのオブジェクトには、

  • ミュータブル(mutable)
  • イミュータブル(immutable)

という性質があります。

簡単に言えば、

オブジェクトそのものを変更できるかどうか

の違いです。

5-1. 整数はイミュータブル

整数型の int はイミュータブルです。

例えば、

x = 10
x = 20

と書いても、10 という整数オブジェクトが 20 に変更されたわけではありません。

x が参照する対象が変わっただけです。

整数オブジェクトは参照先が変更される

このとき、10 という整数オブジェクトそのものは変更されていません。

つまり、int はオブジェクトそのものの値を変更することができないイミュータブルな型です。

5-2. リストはミュータブル

一方、リストはミュータブルです。

numbers = [1, 2, 3]
numbers.append(4)

この場合、

リストオブジェクトそのものが変更される

となります。

ここで注目したいのは、変更前と変更後でIDが同じ「E」になっていることです。

つまり、

同じリストオブジェクトの状態が変更された

と考えることができます。

このように、リストはオブジェクトそのものを変更できるミュータブルな型です。

6. 「なぜ x まで変わるの?」問題

ミュータブルと参照を理解すると、Pythonでよくある疑問も説明できます。

例えば、

x = [1, 2, 3]
y = x

y.append(4)

print(x)
print(y)

を実行すると、

[1, 2, 3, 4]
[1, 2, 3, 4]

となります。

y を変更したのに、なぜ x まで変わったの?」

と思うかもしれません。

その理由は、xy が同じリストオブジェクトを参照しているからです。

xとyが同じリストを参照

y.append(4)は、y という名前そのものを変更しているわけではありません。

y が参照しているリストオブジェクトを変更しています。

そして、そのリストを x も参照しているため、x から見ても変更後のリストが見えるわけです。

ここまでの内容を、

変数は箱ではなく、オブジェクトを参照する名前

という考え方で理解できると、「なぜ x まで変わるのか?」という疑問も説明できるようになります。

7. 「代入」と「コピー」は違う

ここで重要なのが、

y = x

はコピーではないということです。

例えば、

x = [1, 2, 3]
y = x

では、xy は同じリストを参照します。

y = x

一方、

x = [1, 2, 3]
y = x.copy()

とすると、新しいリストオブジェクトを作ることができます。

y = x.copy()

見た目は同じですが、別々のリストオブジェクトです。

そのため、

x = [1, 2, 3]
y = x.copy()

y.append(4)

print(x)
print(y)

を実行すると、

[1, 2, 3]
[1, 2, 3, 4]

となります。

xy が別々のリストオブジェクトを参照しているためです。

このように、代入とコピーは別の操作です。

この違いを理解していないと、リストを扱うときに思わぬ動作に遭遇することがあります。

8. オブジェクトはいつ消えるのか?

ここまで理解すると、もう一つ疑問が出てきます。

例えば、

x = [1, 2, 3]
y = x

のあと、

x = None
y = None

としたら、最初のリストはどうなるのでしょうか。

最初は、

xとyから参照されている状態

x = None
y = None

とすると、xy からリストへの参照がなくなります。

参照する名前がなくなった状態

このように、プログラムから利用できなくなったオブジェクトは、メモリを解放できる状態になります。

Pythonでは、不要になったオブジェクトを自動的に処理する仕組みがあります。

ここで重要になるのが、CPythonの参照カウントです。

9. CPythonの参照カウントとガーベージコレクション

Pythonにはさまざまな実装がありますが、代表的な実装であるCPythonでは、オブジェクトへの参照の数を管理する仕組みとして、参照カウントが使われています。

例えば、

x = [1, 2, 3]
y = x

なら、同じリストを xy が参照しています。

その後、

x = None

とすれば、x からの参照がなくなります。

さらに、

y = None

とすれば、y からの参照もなくなります。

CPythonでは、オブジェクトへの参照がなくなると、参照カウントが0になり、基本的にその時点で破棄されます。

ただし、ここで注意したいのは、

参照カウントだけですべての不要なオブジェクトを処理しているわけではない

ということです。

例えば、オブジェクト同士が互いに参照し合う「循環参照」があると、参照カウントだけでは、そのオブジェクトが不要になったことを判断できない場合があります。

そこでCPythonでは、循環参照などを扱うためのガーベージコレクション(GC)の仕組みも用意されています。

つまり、ざっくり整理すると、

オブジェクト

参照カウントで参照の数を管理

参照カウントが0になる

CPythonでは基本的にオブジェクトが破棄される
※ 循環参照などはGCが処理する

という仕組みになっています。

ここまで理解すると、Pythonが自動的にメモリ管理を行っている理由も少し見えてきます。

10. x = 10 をもう一度考えてみる

ここまでの内容を使って、最初のコードに戻ってみましょう。

x = 10
y = x + 2
print(y)

最初に、

x = 10

によって、x という名前から整数オブジェクトを参照できるようになります。

変数はオブジェクトを参照する名前

次に、

y = x + 2

では、x が参照している整数を使って計算が行われます。

そして、計算結果である 12 を表す整数オブジェクトを、y から参照できるようになります。

y = x + 2

ここで重要なのは、

Pythonの変数そのものが値を持っているのではなく、変数名を通してオブジェクトを参照している

ということです。

この考え方を知っていると、Pythonのコードを読むときに、

「この変数には何が入っているんだろう?」

だけでなく、

「この変数は、どのオブジェクトを参照しているんだろう?」

と考えられるようになります。

11. Pythonの変数を理解すると、いろいろなことがつながる

Pythonの変数を単純な「箱」として考えていると、

  • y = x でなぜ同じリストを参照するのか
  • なぜリストを変更すると別の変数からも変更が見えるのか
  • なぜ int は変更できないのか
  • なぜ copy() が必要なのか
  • オブジェクトはいつメモリから解放されるのか

といった疑問が、それぞれ別々の話に見えてしまいます。

しかし、

「Pythonの変数はオブジェクトへの参照である」

という考え方を軸にすると、これらが一つにつながります。

変数

オブジェクトへの参照

オブジェクトには型・値・識別性がある

ミュータブル / イミュータブル

代入とコピーの違い

参照カウント・ガーベージコレクション

Pythonを少し深く理解したいなら、この「オブジェクトと参照」という考え方はぜひ押さえておきたいポイントです。

まとめ

Pythonの変数について、今回のポイントをまとめます。

Pythonの変数は「箱」ではない

x = 10

は、

x という名前から 10 というオブジェクトを参照する

と考えると、Pythonの仕組みをより正確に理解できます。

Pythonでは、値だけでなくオブジェクトを考える

オブジェクトを理解するときには、

  • 型(type)
  • 値(value)
  • 識別性(identity)

という3つの特徴を意識するとわかりやすくなります。

また、CPythonでは参照カウントによってオブジェクトへの参照が管理され、循環参照などにはガーベージコレクションが使われます。

ミュータブルとイミュータブルがある

int などのイミュータブルなオブジェクトは、オブジェクトそのものを変更できません。

一方、list などのミュータブルなオブジェクトは、オブジェクトそのものを変更できます。

代入とコピーは違う

y = x

は、オブジェクトそのものをコピーする操作ではありません。

x が参照しているオブジェクトを、y も参照するようになります。

一方、

y = x.copy()

では、別のリストオブジェクトを作ることができます。

◆◆◆

Pythonの変数は、最初は「箱」と考えても問題ありません。

しかし、Pythonをさらに深く理解するなら、

「変数は箱ではなく、オブジェクトを参照する名前」

と考えることが大切です。

この考え方を身につけると、Pythonで起こる「なぜこうなるの?」が、少しずつ一本の線でつながってきます。

関連書籍

Pythonの基本文法だけでなく、Pythonという言語そのものをもう少し深く理解したい人には、『入門 Python 3』もおすすめです。

Pythonの文法を学ぶだけでなく、Pythonのオブジェクトやデータモデルについて理解を深める際にも役立ちます。

Amazon.co.jp

コメント

タイトルとURLをコピーしました