Python のオブジェクトについて

オブジェクトとは

Hello World とは、プログラミング言語の世界における伝統的な初歩的なプログラムです。

>>> 'Hello World'
'Hello World'

非常に初歩的な例ですが、この一例は Python におけるオブジェクトの概念を理解する上で重要です。

一見すると、echo コマンドのようにインタプリタが 'Hello World' という文字列を返しているだけに見えます。しかし実際には、インタプリタはまず 'Hello World' という値をもつ文字列型オブジェクトを生成し、そのオブジェクトの文字列表現を標準出力しています。我々が目にしているのは、その結果に過ぎません。

Python では、文字列・整数・真偽値といった基本的なデータ型から、リストや辞書、さらには関数・クラス・例外・モジュールに至るまで、すべてがオブジェクトとして表現されます。

オブジェクトとは、値 (value) と型 (type)、識別子 (id) を持つ透明な箱のようなものです。 透明であるため、我々は自由に箱の中身を覗くことができます。

---------------
|             |
|             |
|    value    |
|             |
| type        |
---------------

文字列オブジェクトや数値オブジェクトは通常、リテラル1を用いて生成されます。

>>> 'Hello World'
'Hello World'
---------------
|             |
|             |
| Hello World |
|             |
| type: str   |
---------------
>>> 1991
1991
---------------
|             |
|             |
|    1991     |
|             |
| type: int   |
---------------

型と識別子

オブジェクトの型は、組み込み関数 type() によって確認できます。

>>> type('Hello World')
<class 'str'>
>>> type(1991)
<class 'int'>
>>> type(type)
<class 'type'>

また、組み込み関数 id() はオブジェクトの識別子を返します。

>>> id('Hello World')
4379060208  # 環境によって異なります
>>> id(1991)
4372760080  # 環境によって異なります

識別子はオブジェクトに対して生存期間中は一意に割り当てられ、実行中は変更されることはありません。

また、多くの実装(特に CPython)では、識別子はメモリ上のアドレスに対応しています。そのため ctypes.cast を利用して、直接取得したメモリアドレスへアクセスすることも可能です。

>>> import ctypes
>>> obj = 'Hello World'
>>> id(obj)
4379060208  # 環境によって異なります
>>> assert ctypes.cast(id(obj), ctypes.py_object).value == 'Hello World'

:::note warn Warning Python では直接的なメモリアクセスは推奨されません。上のコードを書くようなことはまずありません。 :::

変数は箱ではない

変数を「値を格納する箱」のように説明することがありますが、Python の文脈においては、この比喩はあまり正確とはいえません。次のコードを考えてみましょう。

>>> var = 1991
>>> var
1991

正しくは、変数はオブジェクトに紐づいたタグのようなものです。

x 誤ったイメージ: var という名前の箱に 1991 という値が入っている
----------------
|              |
|              |
|     1991     |
|              |
|  var         |
----------------

o 正しいイメージ: 名前 var は数値オブジェクト 1991 を指している
----------------
|              |
|              |
|     1991     |
|              |
| type: int    |-----[var]
----------------

この点は、次の例を見ると明確になります。

>>> a = [1, 2, 3]
>>> b = a
>>> a += [4]
>>> assert b == [1, 2, 3, 4]  # [1, 2, 3] ではない

ab は同じリストオブジェクトを参照しているため、a を通じた変更は b からも観測されます。

より厳密には、変数はオブジェクトに対する参照です。参照が存在する限り、オブジェクトは利用可能であり、参照が失われるとオブジェクトは到達不能になります。オブジェクトがメモリの波に飲み込まれる前に、変数によってオブジェクトを繋ぎ止めておくことができるわけです。

これが分かれば is キーワードと等価演算子 == の違いが理解しやすくなります。

>>> a = [1, 2, 3]
>>> b = a
>>> a += [4]
>>> assert a is b
>>> assert a == b
>>> c = [1, 2, 3, 4]
>>> assert a is not c
>>> assert a == c

前者はオブジェクトの識別子を比較し、後者はオブジェクトの値を比較します。ab は同じオブジェクトを参照しているため、a is b は真です。一方で、ca と同じ値を持つ別のリストオブジェクトであるため、a is c は偽になります。

:::note info Note C++ や一部の Rust の型では、変数を「値を格納する箱」として捉えても問題ない場合があります。ただし Rust においても参照型やヒープ確保された値については同様の概念が登場します。 :::

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

ではこれを踏まえた上で、次のコードの出力を考えてください。

>>> a = 1991
>>> b = a
>>> a += 34
>>> assert a == 2025
>>> b
# 2025? それとも 1991?

正解は 1991 です。あれ?ちょっと待ってください。ab は同じ整数オブジェクトを参照しているのだから、a と同じ 2025 になるのでは??

この疑問を解消するためには、Python の加算代入演算子 += について理解する必要があります。 整数や文字列、タプルのようなイミュータブルなオブジェクトに対する加算代入演算は概ね a = a + 34 と等価です。

>>> a = 1991
>>> b = a
>>> a += 34  # a = a + 34 と等価
>>> assert id(a) != id(b)  # a は新しいオブジェクトを参照するようになった
>>> assert (a, b) == (2025, 1991)

Python における代入文では右辺が先に評価されるので、先に a + 34 が評価され、新しい整数オブジェクトが生成されます。その後、変数 a は新しいオブジェクトを参照するようになります。一方で、変数 b は元の整数オブジェクト 1991 を参照し続けるため、b の値は変更されません。

到達不能なオブジェクトとメモリ解放

次の例では、変数 obj によってオブジェクトへの参照が保持されています。

>>> import ctypes
>>> obj = 'Hello World'
>>> id(obj)
4379060208  # 環境によって異なります
>>> assert ctypes.cast(4379060208, ctypes.py_object).value == 'Hello World'

上記の例では obj という変数を利用してオブジェクトへの参照を保持していますが、以下のようにオブジェクトへの参照が保持されず、オブジェクトへ到達不可能になると、オブジェクトはガベージコレクト(後述)され、メモリから消去されます。

一方で、次のように参照を保持しない場合、生成されたオブジェクトはすぐに到達不能になります。 解放されたオブジェクトのメモリを参照しても期待される出力は得られないでしょう。

>>> import ctypes
>>> id('Hello World')
4379060208
>>> ctypes.cast(4379060208, ctypes.py_object).value
b'e eej¡jFdS'  # 何が起こるか分かりません

この場合、オブジェクトはガベージコレクションによって解放されている可能性があり、結果は未定義です。環境によっては意味のない値が得られたり、例外が発生したりします。

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

Python では、オブジェクトが不要になった時点で自動的にメモリを解放します。この仕組みをガベージコレクションと呼びます。

正確には、オブジェクトへ到達不可能になった時点で、そのオブジェクトはガベージコレクションの対象となります。

到達可能性の判断には、参照カウントという概念が利用されます。一般的に、オブジェクトへの参照が増えると参照カウントが増え、参照が減ると参照カウントが減ります。(CPython 実装では)参照カウントの値が 0 になった時点で、そのオブジェクトはガベージコレクションの対象となります。

sys.getrefcount 関数を利用すると、オブジェクトへの参照カウントを取得できます。

>>> import sys
>>> obj = 'Hello World'
>>> sys.getrefcount(obj)
2
>>> obj2 = obj
>>> sys.getrefcount(obj)
3
>>> obj2 = None
>>> sys.getrefcount(obj)
2

コード 4 行目にて、文字列オブジェクト 'Hello World' に対して新たな参照 obj2 を作成したので、参照カウントが 1 増えたことが確認できます。またコード 6 行目で obj2 は別のオブジェクト None を参照するようになったため、オブジェクトへの参照カウントが 1 減ったことが確認できます。

:::note info Note 最初の sys.getrefcount(obj) の返り値が 2 であることが気になるかもしれません。これは、sys.getrefcount() 関数の引数に渡したオブジェクトへの参照が関数内部で一時的に作成されるためです。 :::

参照カウントが 0 になった時点で、オブジェクトはガベージコレクションの対象となります。実際にガベージコレクトされるタイミングは、Python インタプリタの実装に依存しますが、weakref.finalize を利用すると、ガベージコレクト時に呼び出されるコールバック関数を登録できます。

>>> import weakref
>>> obj = {1, 2, 3}
>>> weakref.finalize(obj, lambda: print('garbage collected!'))
>>> obj = None
garbage collected!

3 行目で、集合オブジェクト {1, 2, 3} に対してガベージコレクト時のコールバック関数が登録されました。その後、4 行目で {1, 2, 3} への参照を失ったことでガベージコレクトされ、コールバック関数が実行されました。

また参照カウントのみが到達可能性の判定に使われるわけではなく、1 以上の参照カウントがあってもそれが循環参照であり、どのみち到達できない場合はガベージコレクトの対象になります。

>>> import weakref
>>> class A:
...     def __init__(self):
...         self.b = None
...
>>> class B:
...     def __init__(self):
...         self.a = None
...
>>> obj1 = A()
>>> obj2 = B()
>>> obj1.b = obj2
>>> obj2.a = obj1
>>> weakref.finalize(obj1, lambda: print('garbage collected!'))
>>> obj1 = obj2 = None
garbage collected!

この例では、クラス A とクラス B のインスタンスが、プロパティを通じてお互いを参照し合う循環参照の関係にあります。しかし、最後に obj1obj2 の両方の参照を失うと、A オブジェクトと B オブジェクトはどちらも到達不能になります。

このように、参照カウントを残しながらも、オブジェクトはガベージコレクトされました。これは外部から到達不可能であるため、循環参照検出機構によってどちらも到達可能性がないと判断されたためです。

まとめ

本エントリでは、Python におけるオブジェクトの概念について説明しました。Python では、すべてがオブジェクトとして表現され、変数はあくまでオブジェクトへの参照にすぎません。この事実を直感的に理解するために、オブジェクトの型、識別子、参照カウント、ガベージコレクションといった概念を紹介しました。 今後 Python のデータモデルを本格的に学習する際、本エントリが一助となれば幸いです。

参考

2025 年 11 月に O’Reilly Japan から Fluent Python 第 2 版の日本語訳版 が出版されました。Python の美学について深く掘り下げた素晴らしい書籍であり、また読み物としても非常に面白いです。 もし Python の基本文法を一通り学習された後であればご一読されることを強くお勧めします。


  1. リテラルとは、組み込み型のオブジェクトを生成するための記法です。例えばクォーテーションで任意の Unicode 文字を囲むと文字列オブジェクトが生成されます。 また 0 以外から始まる数字を並べると整数オブジェクトが生成されます。 ↩︎