LoginSignup
10
9

More than 1 year has passed since last update.

Pythonのオブジェクトへの参照とガベージコレクション

Last updated at Posted at 2021-11-28

概要

CPythonの実装を読んでいると、Pythonのオブジェクト(内部でPyObjectと言われるやつ)への参照の数を管理する場面が多いことがわかります。この記事では、なぜそのようなことをするのかについてまとめます。

なぜPythonのオブジェクトは参照の数を持つのか

Pythonのガベージコレクションは、①オブジェクトの参照のカウントに基づいたものと、②世代管理に基づいたもの、の二種類があります。
前者の方式を実現するために、Pythonのオブジェクトはすべて、自身が他のいくつの変数から参照されているかと言う情報を保持しています。

ちなみにこの参照数の情報は、Pythonのsysモジュールのgetrefcountという関数で見ることができます。

import sys

test_val = {"test":"test"}        # 変数の作成
print(sys.getrefcount(test_val))  # 2 -> 変数自身と、getrefcount関数からの参照

copy_val = test_val               # 別の変数にtest_valをコピー
print(sys.getrefcount(test_val))  # 3 -> copy_valからも参照されたため数が増えた

del copy_val                      # コピーした変数を削除
print(sys.getrefcount(test_val))  # 2 -> copy_valからの参照が消えたため

このオブジェクトへの参照の数が0になれば、そのオブジェクトはどこからもアクセスされていないことになるので、ガベージコレクションの対象となります。このことは、Pythonのオブジェクト一般の実装をしているヘッダーファイル (Include/object.h) 内のコメントで明記されています (3.10版のソースコードだと、415行目あたり)。

Py_DECREF calls the object's deallocator function when
the refcount falls to 0; for
objects that don't contain references to other objects or heap memory
this can be the standard function free(). 

訳すと、以下のような感じ

Py_DECREF (訳注:マクロ名) は、参照数が0となるとそのオブジェクトのメモリ開放関数を呼び出します。
この関数は、他のオブジェクトもしくはヒープメモリーへの参照を持たないオブジェクトの場合は、Cの標準ライブラリのfree関数となります。

参照の数のカウントの増減

増減の実装

上のコメントで登場するPy_DECREFマクロのなかで利用される関数_Py_DECREFでは、参照のカウントがゼロになったときにメモリ開放をしているようです (488行目から)。

object.h
static inline void _Py_DECREF(
#if defined(Py_REF_DEBUG) && !(defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030A0000)
    const char *filename, int lineno,
#endif
    PyObject *op)
{
#if defined(Py_REF_DEBUG) && defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030A0000
    // Stable ABI for Python 3.10 built in debug mode.
    _Py_DecRef(op);
#else
    // Non-limited C API and limited C API for Python 3.9 and older access
    // directly PyObject.ob_refcnt.
#ifdef Py_REF_DEBUG
    _Py_RefTotal--;
#endif
    if (--op->ob_refcnt != 0) {
#ifdef Py_REF_DEBUG
        if (op->ob_refcnt < 0) {
            _Py_NegativeRefcount(filename, lineno, op);
        }
#endif
    }
    else {
        _Py_Dealloc(op);
    }
#endif
}

上のコード、デバッグモード時の挙動の記述が長いのですが、それらを省くと以下のようにすっきりして挙動が追いやすくなります。

object.h
static inline void _Py_DECREF(PyObject *op)
{
    if (--op->ob_refcnt == 0) {
        _Py_Dealloc(op);
    }
}

Pythonのオブジェクトを表す構造体 (PyObject)の、参照数を持つメンバー (ob_refcnt) をデクリメントして値が0になったら、メモリ開放を行う関数_Py_Dealloc()を呼び出してます。

逆に、参照数が増えるときは、単純にオブジェクトの参照カウントを増やしているだけで、他の処理はされません (472行目から)。

object.h
static inline void _Py_INCREF(PyObject *op)
{
#if defined(Py_REF_DEBUG) && defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030A0000
    // Stable ABI for Python 3.10 built in debug mode.
    _Py_IncRef(op);
#else
    // Non-limited C API and limited C API for Python 3.9 and older access
    // directly PyObject.ob_refcnt.
#ifdef Py_REF_DEBUG
    _Py_RefTotal++;
#endif
    op->ob_refcnt++;
#endif
}

デバッグモードでなければ、op->ob_refcnt++を実行しているだけです。

増減処理が呼ばれるとき

実際に上の参照数の増減処理が呼び出される場面をいくつか例で出します。

tupleから要素を取得

tupleから要素を一つインデックスで指定して取得する場合の処理です。

tupleobject.c
static PyObject *
tupleitem(PyTupleObject *a, Py_ssize_t i)
{
    if (i < 0 || i >= Py_SIZE(a)) {
        PyErr_SetString(PyExc_IndexError, "tuple index out of range");
        return NULL;
    }
    Py_INCREF(a->ob_item[i]);  // <- これ
    return a->ob_item[i];
}

引数として受け取ったtupleの中の一要素 (ob_item[i]) を返す前に、Py_INCREF(a->ob_item[i])としてその要素への参照数を増やしています。

float型変数の値が整数値か判定

float型の変数に対してis_integer()を実行した時の処理です。

floatobject.c
static PyObject *
float_is_integer_impl(PyObject *self)
/*[clinic end generated code: output=7112acf95a4d31ea input=311810d3f777e10d]*/
{
    double x = PyFloat_AsDouble(self);
    PyObject *o;

    if (x == -1.0 && PyErr_Occurred())
        return NULL;
    if (!Py_IS_FINITE(x))
        Py_RETURN_FALSE;
    errno = 0;
    o = (floor(x) == x) ? Py_True : Py_False;
    if (errno != 0) {
        PyErr_SetFromErrno(errno == ERANGE ? PyExc_OverflowError :
                             PyExc_ValueError);
        return NULL;
    }
    Py_INCREF(o);  // <- これ
    return o;
}

この関数は判定を行うものですが、返り値は単純なCのプリミティブな値ではなくて、Pythonのオブジェクトとなります。そのため、返すオブジェクトに対してPy_INCREF()を行って、参照数を増やしています。

辞書への要素の追加

辞書に要素を新しく追加する処理です。

dictobject.c
int
PyDict_SetItem(PyObject *op, PyObject *key, PyObject *value)
{
    if (!PyDict_Check(op)) {
        PyErr_BadInternalCall();
        return -1;
    }
    assert(key);
    assert(value);
    Py_INCREF(key);    // <- これ
    Py_INCREF(value);  // <- これ
    return _PyDict_SetItem_Take2((PyDictObject *)op, key, value);
}

実際の追加の処理は別の関数(_PyDict_SetItem_Take2)でやっているようですが、そちらを呼び出す前に先にkeyとして指定されたオブジェクトとvalueとして指定されたオブジェクトの両方の参照を増やしています。

ちなみに、呼び出し先の関数で辞書への追加の処理に失敗した場合は、きちんとkeyvalueへの参照数を減らしてます。

dictobject.c
int
_PyDict_SetItem_Take2(PyDictObject *mp, PyObject *key, PyObject *value)
{
    assert(key);
    assert(value);
    assert(PyDict_Check(mp));
    Py_hash_t hash;
    if (!PyUnicode_CheckExact(key) ||
        (hash = ((PyASCIIObject *) key)->hash) == -1)
    {
        hash = PyObject_Hash(key);
        if (hash == -1) {
            Py_DECREF(key);    // <- これ
            Py_DECREF(value);  // <- これ
            return -1;
        }
    }
    if (mp->ma_keys == Py_EMPTY_KEYS) {
        return insert_to_emptydict(mp, key, hash, value);
    }
    /* insertdict() handles any resizing that might be necessary */
    return insertdict(mp, key, hash, value);
}

処理に失敗したと思われるときには、-1を返却して呼び出し元に失敗を通知する前に、Py_DECREFを呼び出してます。

rangeオブジェクトの作成

for文回すときとかに使うあれを作る処理です。

rangeobject.c
static rangeobject *
make_range_object(PyTypeObject *type, PyObject *start,
                  PyObject *stop, PyObject *step)
{
    rangeobject *obj = NULL;
    PyObject *length;
    length = compute_range_length(start, stop, step);
    if (length == NULL) {
        return NULL;
    }
    obj = PyObject_New(rangeobject, type);
    if (obj == NULL) {
        Py_DECREF(length);  // <- これ
        return NULL;
    }
    obj->start = start;
    obj->stop = stop;
    obj->step = step;
    obj->length = length;
    return obj;
}

rangeオブジェクトはCPythonの実装上では、start, stop, step, lengthというオブジェクトをメンバーとして持つ構造体です。この関数では引数として受け取ったstart, stop, stepからlengthを計算していますが、rangeオブジェクトの作成に失敗してNULLを返す場合には、この新しく生成したlengthオブジェクトは不要になるため、参照数を減らして、ガベージコレクションの対象として削除をさせています(Py_DECREF(length))。

まとめ

Pythonのオブジェクトは自身への参照の数を情報として保持していて、それが0になるとメモリ開放がなされる仕組みとなっています。そのため、CPythonの実装のあらゆるところでこの参照数の情報をいじる処理が発生しています。

10
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
9