1.10.3 薄氷

数少ない状況において、一見無害に見える借用参照の利用が問題をひきおこす ことがあります。この問題はすべて、インタプリタが非明示的に呼び出され、 インタプリタが参照の所有者に参照を放棄させてしまう状況と関係しています。

知っておくべきケースのうち最初の、そして最も重要なものは、 リスト要素に対する参照を借りている際に起きる、 関係ないオブジェクトに対するPy_DECREF() の使用です。 例えば:

void
bug(PyObject *list)
{
    PyObject *item = PyList_GetItem(list, 0);

    PyList_SetItem(list, 1, PyInt_FromLong(0L));
    PyObject_Print(item, stdout, 0); /* BUG! */
}

上の関数はまず、list[0] への参照を借用し、次にlist[1] を値 0 で置き換え、最後にさきほど借用した参照を出力 しています。何も問題ないように見えますね? でもそうではないのです!

PyList_SetItem() の処理の流れを追跡してみましょう。 リストは全ての要素に対して参照を所有しているので、要素 1 を 置き換えると、以前の要素 1 を放棄します。ここで、以前の要素 1 がユーザ定義クラスのインスタンスであり、さらにこのクラスが __del__() メソッドを定義していると仮定しましょう。 このクラスインスタンスの参照カウントが 1 だった場合、 リストが参照を放棄すると、インスタンスの __del__() メソッドが呼び出されます。

クラスは Python で書かれているので、__del__() は任意の Python コードを実行できます。この __del__()bug() における item に何か不正なことをして いるのでしょうか? その通り! buf() に渡したリストが __del__() メソッドから操作できるとすると、"del list[0]"の効果を持つような文を実行できてしまいます。もしこの操作で list[0] に対する最後の参照が放棄されてしまうと、 list[0] に関連付けられていたメモリは解放され、 結果的に item は無効な値になってしまいます。

問題の原因が分かれば、解決は簡単です。 一時的に参照回数を増やせばよいのです。 正しく動作するバージョンは以下のようになります:

void
no_bug(PyObject *list)
{
    PyObject *item = PyList_GetItem(list, 0);

    Py_INCREF(item);
    PyList_SetItem(list, 1, PyInt_FromLong(0L));
    PyObject_Print(item, stdout, 0);
    Py_DECREF(item);
}

これは実際にあった話です。以前のバージョンの Python には、 このバグの一種が潜んでいて、__del__() メソッドが どうしてうまく動かないのかを調べるために C デバッガで相当 時間を費やした人がいました...

二つ目は、借用参照がスレッドに関係しているケースです。 通常は、 Python インタプリタにおける複数のスレッドは、 グローバルインタプリタロックがオブジェクト空間全体を保護している ため、互いに邪魔し合うことはありません。とはいえ、ロックは Py_BEGIN_ALLOW_THREADS マクロで一時的に解除したり、 Py_END_ALLOW_THREADS で再獲得したりできます。 これらのマクロはブロックの起こる I/O 呼び出しの周囲によく置かれ、 I/O が完了するまでの間に他のスレッドがプロセッサを利用できるように します。明らかに、以下の関数は上の例と似た問題をはらんでいます:

void
bug(PyObject *list)
{
    PyObject *item = PyList_GetItem(list, 0);
    Py_BEGIN_ALLOW_THREADS
    ...ブロックが起こる何らかの I/O 呼び出し...
    Py_END_ALLOW_THREADS
    PyObject_Print(item, stdout, 0); /* BUG! */
}

ご意見やご指摘をお寄せになりたい方は、 このドキュメントについて... をご覧ください。