Pythonの参照カウント管理

2007/03/30 (Fri) 22:07:46 JST

Pythonの拡張ライブラリでの参照カウント(リファレンスカウント)の管理についてのメモです。Pythonではオブジェクトのメモリ管理を、参照カウントと参照カウントを補佐するGCで行っています。RubyのようにすべてGCが面倒を看てくれたり、Cocoaのように?NSAutoreleasePoolがカウント管理の大半を肩代わりしてくれたりすることもなく、けっこう面倒です。せめて?NSAutoreleasePool相当のものがあったらすこぶる楽できるんですけど。

参照カウントのAPI

参照カウントを増減するには、それぞれ Py_INCREF(x), Py_DECREF(x) のマクロを使います。オブジェクトが NULL である可能性がある場合は、 Py_XINCREF(x), Py_XDECREF(x) を使います。

参照カウント値はオブジェクト構造体に含まれています。obj->ob_refcnt で求めることができますが、カウント値を調べて増減を調整するなどのカウント値に依存する実装は避けましょう。

参照カウントを増減する主な操作

オブジェクトの生成

PyObject_New() などで生成した新しいオブジェクトは、カウントが 1 に設定されます。新しいオブジェクトは生成した関数内でのみ参照されているので、明示的にカウントを減らさない限り解放されません。

PyObject *list;

list = PyList_New(0); /* カウントは 1 */
Py_DECREF(list);      /* 0 になるので回収される */

生成したオブジェクトを戻り値にする場合は、もちろんカウントを減らしてはいけません。

return PyList_New(0); /* カウントを 1 のままにしておく */

戻り値

スクリプトから呼ばれる関数の戻り値は、カウントを増やしておく必要があります。例として、引数を一つ受け取り、その引数をそのまま戻り値にする関数を実装します。この関数は次のように使うとします。

import testmodule

value = testmodule.yourself('hello')
print value # 'hello' と表示される

実装はこうなります。

static PyObject *
testmodule_yourself(PyObject *self, PyObject *args)
{
  PyObject *value;

  /* 引数をローカル変数に代入 */
  if (!PyArg_ParseTuple(args, "O", &value))
    return NULL;
  Py_INCREF(value);
  return value;
}

?PyArg_?ParseTuple はカウントを操作しません。処理内容も引数をそのまま返すだけですので、カウントを増やすのはバランスが悪いように見えます。試しに Py_INCREF の部分をコメントアウトしてコンパイルし、先のスクリプトを実行したところでは落ちませんでした。じゃあ Py_INCREF は必要ないのかというとそうでもありません。次のように新しいクラスのインスタンスを使って、かつ何度も繰り返すと落ちます。

import testmodule

class A: pass
for x in xrange(1000):
  value = testmodule.yourself(A())

この例では二つの罠があります。

二つ目がポイントです。戻り値のカウントを増やさない場合のループ処理を詳しく追ってみましょう。行先頭のかっこ内の値がカウントです。

  1. (1) A(): クラスAのインスタンスを生成する。
  2. (1) yourself(A()):そのインスタンスを引数として yourself() を呼び出す。
  3. (1) インスタンスを yourself() の戻り値にする。
  4. (0) yourself() が終了、引数に使ったオブジェクトのカウントを減らす。
  5. (1) 変数 value に代入される。
  6. (0) 変数 value はループ内でのみ有効。ループの終わりにオブジェクトのカウントを減らし、変数を破棄する。
  7. ループの先頭に戻る。

途中でカウントが 0 になることがわかります。0 になった時点でオブジェクトが回収されてしまい、そのオブジェクトのカウントを増やそうとしてエラーになります。

スコープの広い変数への代入

オブジェクトの属性など、ローカル変数よりもスコープの広い変数にオブジェクトを代入するときは、すでに代入されているオブジェクトとこれから代入するオブジェクトのカウントを調整する必要があります。次のコードはPythonのドキュメントから抜粋した初期化用の関数です。引数のオブジェクトを構造体のメンバに代入しています。

typedef struct {
  PyObject_HEAD
  PyObject *first;
  PyObject *last;
  int number;
} Noddy;

static int
Noddy_init(Noddy *self, PyObject *args, PyObject *kwds)
{
  PyObject *first=NULL, *last=NULL, *tmp;

  static char *kwlist[] = {"first", "last", "number", NULL};

  if (! PyArg_ParseTupleAndKeywords(args, kwds, "|OOi", kwlist, 
                                    &first, &last, 
                                    &self->number))
    return -1; 

  if (first) {
    tmp = self->first;
    Py_INCREF(first);
    self->first = first;
    Py_XDECREF(tmp);
  }

  if (last) {
    tmp = self->last;
    Py_INCREF(last);
    self->last = last;
    Py_XDECREF(tmp);
  } 

  return 0;
}

self->first と self->last を操作しているコードを見てください。

if (first) {
  tmp = self->first;
  Py_INCREF(first);    /* 代入するオブジェクトのカウント増 */
  self->first = first; /* 代入 */
  Py_XDECREF(tmp);     /* 以前のオブジェクトのカウント減 */
                       /* 初期値はNULLなので Py_XDECREF を使う */
}

新しいオブジェクトを self->first に直接代入せず、すでに代入されているオブジェクトをローカル変数に移しています。次に新しいオブジェクトのカウントを増やし、今まで代入されていたオブジェクトのカウントを減らします。先に新しいオブジェクトのカウントを増やすのは、代入前のオブジェクトと同一である場合を考慮しての処理です。代入前のオブジェクトのカウントを先に減らしてしまうと解放されてしまう可能性があります。これを避けるためには次のように書くこともできますが、条件式の比較の分だけ余計な処理を行うことになり、あまり意味がありません。

if (first && self->first != first) {
  Py_XDECREF(self->first);
  Py_INCREF(first);
  self->first = first;
}

カウントを減らすのに Py_DECREF ではなく Py_XDECREF が使われていますが、これは代入前の値がNULLの可能性があるからです。

コレクション

コレクション(シーケンス型、マップ型)にオブジェクトを追加すると、そのオブジェクトのカウントが増やされます。これは要素が他から解放されないようにするためです。オブジェクトをコレクションに追加した後、特に保持しておく必要がなければオブジェクトのカウントを減らします。コレクションへの追加前にカウントを減らしてしまうと、その時点で回収されてしまう可能性があるので注意しましょう。

l = PyList_New(1);
s = PyString_FromString("hi"); /* カウントは 1 */
PyList_Append(l, s);           /* ここで s のカウントは 2 になる */
Py_DECREF(s);                  /* s のカウントを 1 に戻す */
                               /* あとは s の解放をリストにお任せ */

イテレータ

イテレータは PyObject_GetIter() で取得することができます。詳しくは後述しますが、この関数で取得したイテレータは自分で管理する必要があります。イテレータ自身を戻り値にしない場合は、最後にカウントを減らして総カウント数を調整しなければいけません。イテレータを取得した時点のカウントは 1 になるので、減らすと同時に回収されます。

イテレータから要素を一つ一つ取り出すには PyIter_Next() を使います。コレクションと同様に、イテレータを通して取得した要素のカウントは一つ増えています。ただしコレクションとは異なり、後でイテレータを解放しても各要素のカウントは変化しません。そのため、次の要素を取り出す前に現在の要素のカウントを減らしておく必要があります。

/* イテレータを取得 */
it = PyObject_GetIter(seq);
while ((x = PyIter_Next(it)) != NULL) {
  ...
 /*
  * 要素 x はイテレータによってカウントが増えている
  * 次の要素を取り出す前にカウントを減らしておく 
  */
  Py_DECREF(x);
}
Py_DECREF(it); /* イテレータを解放 */

メモリリークする例:

/* イテレータを取得 */
it = PyObject_GetIter(seq);
while ((x = PyIter_Next(it)) != NULL) {
  ...
}
/*
 * イテレータを解放しても各要素のカウントは減らない
 * 各要素は解放されずにメモリリークの原因になる
 */
Py_DECREF(it);

所有権(オーナーシップ)

カウントを増減する操作を次にまとめます。

カウントが増減する時期ではなく、「誰が」カウントを増減させるのかに注目してください。その中でも特に「誰がカウントを減らすのか」が重要です。これが参照カウントの所有権(オーナーシップ)、つまり(カウントを減らして)オブジェクトを解放する権利のことです。カウントを減らすことのできる関数は、オブジェクトの参照の所有権を持っていることになります。このような関数を所有者と呼びます。関数がメソッドであれば、メソッドを持つオブジェクトが所有者になります。ただし、参照の所有者は一つではありません。もしカウントが50であれば、その参照に対して50の所有者がいることになります。

所有権はデータではないことに注意してください。オブジェクトには参照の所有権の有無を保持する要素も、所有権を調べるAPIもありません。カウントを増やせば所有権を持つことになり、減らせば放棄することになります。また、カウントが1以上か調べる方法もありません。カウント数は?PyObjectのob_refcntメンバで求めることができますが、オブジェクトが解放されていれば、そのメンバも解放されたメモリ上にあることになるからです。

所有権の放棄

次に、カウントの増減するタイミングを見てみます。カウントの増減は必ずセットになります。

カウントを増やしてから減らすまでが、オブジェクトの参照の所有者になります。オブジェクトをコレクションに追加するとカウントが増え、コレクションも参照の所有者になります。コレクションに追加する前にカウントを減らすと、代入時と同じく回収されてしまう可能性があるので、追加後に減らすようにしましょう。

所有参照と借用参照

上に挙げた操作のように自分が所有権を持つ参照を所有参照、持たない参照を借用参照と呼びます。代表的な借用参照は引数です。引数を分解する一般的なコードを見てください。

static PyObject *
function(PyObject *self, PyObject *args)
{
  PyObject *value;

  /* 引数をローカル変数に代入 */
  if (!PyArg_ParseTuple(args, "O", &value))
    return NULL;

  ...
}

この関数の引数は self と args です。self はこの関数がメソッドとして呼ばれたときのオブジェクトで、 args は引数をまとめたタプルです。?PyArg_?ParseTupleで引数を分解します。

インスタンス変数にするなどして保持しない限り、引数のカウントを増減させる必要はありません(分解後のオブジェクトも同様)。関数が終了してもその参照を保持する必要がなければ、カウントを増やして所有者になる必要はありません。このように、借用参照はカウントを管理せずに済むので扱いが簡単です。

関数の戻り値が所有参照か借用参照かは、ドキュメントに書いてあります。例えばタプルを生成する PyTuple_New は所有参照(新しい参照)を返します。この関数を呼んだ関数が参照の所有者となるので、不要になったらカウントを減らさなければいけません。一方、タプルの要素を取得する PyTuple_GetItem は借用参照(借りた参照)を返します。関数終了後も保持しなければカウントを操作する必要はありません。

参照カウント管理の原則とパターン

参照カウント管理は一見複雑ですが、その原則はごく簡単です。

  1. 自分で生成したオブジェクトは自分で解放する。
  2. 他人から受け取った(他人が生成した)オブジェクトのカウントは操作しない。
  3. 他人から受け取ったオブジェクトが必要ならカウントを増やし、不要になったらカウントを減らす。

この原則を元に、代表的なパターンをまとめます。

if (first) {
  Py_INCREF(first);
  Py_XDECREF(self->first);
  self->first = first;
}

/* 面倒ならマクロを定義 */
#define ASSIGN(var, value) ¥
  Py_INCREF(value); ¥
  Py_XDECREF(var); ¥
  var = value;

if (first)
  ASSIGN(self->first, first);
if (it = PyObject_GetIter(obj))
  return NULL;
while ((x = PyIter_Next(it)) != NULL) {
  ...
  Py_DECREF(x) /* ループの最後でカウントを減らす */
}

参考リンク

Pythonの参照カウント法
http://www.python.jp/doc/nightly/ext/refcounts.html
参照カウントのAPI
http://www.python.jp/doc/nightly/api/countingRefs.html
Cocoaのメモリ管理
http://wwwa.dcns.ne.jp/~nito/CocoaClub/article01.html

Inverse Pages: Python