【Python/C API】所有権と参照カウントについて解説

所有権と参照カウントの解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、Python/C API における所有権と参照カウントについて解説していきたいと思います。

所有権については、下記ページで Python 公式から明確な説明が行われています。

ただ、所有権は Python/C API での参照カウントの扱いを理解する上では非常に重要なものですので、もうちょっと砕けた言い方や図などで説明を補うなどして、所有権についてこのページでも解説をしてみることにしました。

この所有権の考え方を理解すれば、Python/C API を利用する際に Py_INCREFPy_DECREF をいつ実行すれば良いかが明確に分かるようになると思います。

Python/C API を利用した Python の拡張や Python からのC言語の関数の呼び出しを行う際には役立つ知識になると思いますので、こういったことに挑戦してみたい方は是非読んでみてください。

ちなみに、「Python からのC言語の関数呼び出し」については下記ページで解説しています。一度 Python/C API を利用したC言語の関数呼び出しを経験しておくと、このページの解説内容がわかりやすくなると思いますので、特に Python/C API を利用したことがない方には事前に下記ページを読んでいただくことをオススメします。

PythonからC言語の関数を呼び出す方法の解説ページアイキャッチ PythonからC言語の関数を呼び出す(基本編)

オブジェクトと参照

まず前提として、Python スクリプトにおいては全てのデータはオブジェクトとして扱われます。そしてオブジェクトを使用する際には、オブジェクトを変数で参照して使用することになります。

例えば下記を実行すれば、C言語の場合は変数 x に整数 15 を格納することになりますが、Python においては変数 x が整数 15 のオブジェクトを参照することになります。

CとPythonの違い
x = 15

また、CPython では Python インタプリタがC言語で記述して開発されています。なので、この「オブジェクト」や「参照」もC言語で記述して実現されています。

大雑把に考えれば、C言語の界面でいうオブジェクトとは「PyObject 型」or「PyObject を拡張した構造体型」のデータと考えられます。以降では、PyObject を拡張した構造体型も含めて PyObject 型と呼ばせていただきます。

それに対し、参照とは、オブジェクト(PyObject 型のデータ)を指すポインタ変数と考えられます。つまり、ポインタ変数に対して PyObject 型のデータの先頭アドレスを格納した際に、そのポインタ変数はオブジェクトを参照することになります。

オブジェクトと参照の説明図

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

また、Python にはガベージコレクションという機能が存在します。この機能があるため、Python スクリプトではオブジェクトを明示的に解放する必要がありません。

スポンサーリンク

ガベージコレクション

C言語の場合、malloc 関数で確保したメモリは明示的に free 関数で解放する必要がありますが、Python スクリプトではガベージコレクションによって不要になったオブジェクトは自動的に解放されるようになっています。そのため、free 関数の実行などといった明示的な解放は不要です。

そして、このガベージコレクションの機能も、CPython の場合はC言語で記述して実現されていることになります。

参照カウント

具体的には、各オブジェクト(PyObject 型のデータ)は参照カウントというデータを持っており、この参照カウントが 0 になった際に自動的にそのオブジェクトが解放されるようになっています。

オブジェクトの参照カウントは、そのオブジェクトへの参照を Py_INCREF の引数に指定して実行することでインクリメントすることができます。Py_INCREF は関数マクロになっています。

前述の通り、オブジェクトへの参照とは PyObject 型のデータを指すポインタ変数と考えられますので、ポインタ変数を引数に指定して Py_INCREF を実行すれば、そのポインタ変数の指すオブジェクト(PyObject 型のデータ)の参照カウントがインクリメントされることになります。

Py_INCREFの説明図

逆に、オブジェクトの参照カウントは、そのオブジェクトへの参照を Py_DECREF の引数に指定して実行することでデクリメントすることができます。Py_DECREF は関数マクロになっています。

Py_DECREFの説明図

そして、そのデクリメントされた際に参照カウントが 0 になった場合、Py_DECREF の中でオブジェクトの解放処理が行われるようになっています。

Py_DECREFでオブジェクトが解放される様子

C言語でオブジェクト(PyObject 型のデータ)を扱う際には、free 関数の実行を行わなくても良い代わりに、これらの Py_INCREFPy_DECREF を適切なタイミングで実行する必要があります。

MEMO

Py_INCREFPy_DECREF については下記ページで解説されています

https://docs.python.org/ja/3.10/c-api/refcounting.html

基本的に、このページでは Py_INCREFPy_DECREF を使って説明をしていきますが、引数で指定するポインタが NULL である可能性がある場合は、Py_XINCREFPy_XDECREF を利用する必要があります

Py_INCREF と Py_DECREF の実行タイミング

では、Py_INCREF や Py_DECREF は具体的にどんなタイミングで実行すれば良いのでしょうか?

この実行タイミングとして、オブジェクトへの参照が発生する際に毎回 Py_INCREF を実行し、オブジェクトへの参照が不要になった際 or 参照が無くなる際に毎回 Py_DECREF を実行することが考えられます。

ただ、これを実装するのはかなり大変です。

例えばオブジェクトへの参照が無くなるタイミングとして関数終了時が挙げられます。ローカル変数の PyObject * 型のポインタからオブジェクトを参照している場合、関数終了時にローカル変数は解放されるため、その時にオブジェクトへの参照も無くなることになります。

なので、関数内で複数の箇所から return を実行する場合、その全ての return の前で Py_DECREF を実行するような処理が必要になることになります。

これは、C言語で malloc で確保したメモリを free 関数で解放する時のソースコードを考えると分かりやすいと思います。下記の free 関数と同じような感じで、不要になる参照に対して漏れなく Py_DECREF を実行してから return を行う必要があります。

解放漏れを防ぐ
int *data1 = malloc(sizeof(int) * 100);
if (data1 == NULL) {
    return -1;
}

int *data2 = malloc(sizeof(int) * 100);
if (data2 == NULL) {
    free(data1);
    return -1;
}

int *data3 = malloc(sizeof(int) * 100);
if (data3 == NULL) {
    free(data1);
    free(data2);
    return -1;
}

以下略

そのため、Python インタプリタや Python のモジュールのソースコードでは上記のように毎回 Py_INCREFPy_DECREF を実行するのではなく、適切かつ最小限のタイミングでのみ実行するようになっています。

そして、その適切なタイミングは、次に説明する「参照の所有権」の考え方に則って決められます。

Python/C API もこの考えに則って Py_INCREFPy_DECREF を実行するようになっており、これらを利用する側の関数を実装する際も、「参照の所有権」の考え方に則って Py_INCREFPy_DECREF を実行するようにした方が良いです。

そのため、Python/C API を利用して Python を拡張するような際や、Python からC言語を呼び出す際のラッパー関数を実装する際には、この「参照の所有権」の考え方が非常に重要になります。

スポンサーリンク

参照の所有権

次は、この「参照の所有権」について解説していきます。

所有権と所有権のルール

参照の所有権とは、関数が獲得できる権利と義務になります。この所有権は参照とセットで管理されます。

具体的には、この参照の所有権とは、その参照の所有権を「放棄することができる」という権利になります。

と同時に、関数が終了するまでにその参照の所有権を「放棄しなければならない」という義務でもあります。

参照の所有権を獲得することで、関数には上記の権利と義務が発生します。

MEMO

所有権の獲得放棄については追って解説をしていきます

ちなみに、前述で紹介した Py_INCREF は所有権を獲得するための1つの手段であり、Py_DECREF は所有権を放棄するための1つの手段となります

要は、参照の所有権を持つ関数は、関数が終了までにその参照の所有権を放棄しなければなりません。逆に、参照の所有権を持たない関数は、その参照の所有権を放棄してはいけません。

これは所有権の考え方に基づいたルールです。以降、このルールを所有権のルールと呼ばせていただきます。

簡単に言えば、所有権とは、所有権を放棄する関数を明確にするためのもの(考え方)です。

C言語でも malloc 関数で確保したメモリを色んな関数で使い回すような場合、どの関数で free して解放するかをしっかり考えて設計・実装する必要がありますよね?

同様に、所有権は、その解放を行う関数を明確化にするためのものであると考えるとイメージしやすいかと思います。

後述でも解説するように、所有権のルールを守って実装を行えば、Py_INCREFPy_DECREF を毎回実行することなくメモリリークや解放済みのオブジェクトへのアクセスを防ぐことができます。

例えば下の図は、関数がローカル変数 x でオブジェクト obj1 を、ローカル変数 y でオブジェクト obj2 を、ローカル変数 z でオブジェクト obj3 をそれぞれ参照している様子を示しています。

オブジェクトへの参照とは、前述の通りオブジェクトを指す PyObject *  型のポインタです。

関数がオブジェクトXへの参照の所有権を持ち他のオブジェクトへの参照の所有権を持たない様子

この関数はオブジェクト obj1 への参照の「所有権」を所持しています。その他のオブジェクトへの参照の所有権は所持していません。

そのため、この関数はオブジェクト obj1 への参照の所有権のみを放棄することができます。その他のオブジェクトへの参照の所有権は放棄できません。

関数が放棄できる所有権を示す図

また、この関数は関数終了までに必ずオブジェクト obj1 への参照の所有権を放棄する必要があります。

ちなみに、上記の x のような関数が所有権を持つ参照のことを「所有参照」、関数が所有権を持たない参照のことを「借用参照」と呼びます。

ルールを守るのは開発者

ここまでの説明で「参照の所有権を所持していない関数は所有権を放棄できない」と聞いて当たり前の話だと感じた方も多いと思います。だって所有権を所持していないんだから放棄のしようがないですよね…。

ですが、これはプログラムを実装する上では当たり前の話ではありません。なぜなら、所有権のルールを破るような処理の実装もやろうと思えば出来てしまうからです。例えば、所持していない所有権を放棄するような処理を実装することも可能です。

所有権のルールを破ったとしても、つまり所有権を所持していない関数が所有権を放棄したり、所有権を所持している関数が所有権を放棄しなかったりしたとしても、コンパイルでエラーが発生することはありません。

所持していない参照の所有権を放棄指定しまう様子

そもそも所有権は変数などのようにソースコードやプログラムの中で目に見える形で存在するものではありません。所有権は開発者の頭の中やドキュメントの中にのみ存在する考え方です。

そのため、コンパイル時に所有権に関するルールが守られているかどうかはチェックすることはできません。

なので、簡単に所有権のルールを破ることができます。

ただし、ルールを破った瞬間、オブジェクトの解放漏れや解放済みのオブジェクトへのアクセスが発生する可能性が生じます。

そのため、安全なプログラムを実現していくためには、Python のオブジェクトを扱うプログラムの開発者は所有権や所有権のルールを理解し、開発中の関数の所有権の有無に基づいてルールをしっかり守りながら実装していく必要があります。

難しそうですが、慣れてくると所有権を意識した実装はそこまで苦にならないと思います。

スポンサーリンク

参照の所有権と参照カウント

また、参照の所有権の考え方を取り入れた場合、オブジェクトの参照カウントとは、そのオブジェクトへの参照の所有権の残数と考えることができます。

そして、Py_INCREF は引数で指定された参照の所有権を発行する関数マクロと考えることができます(所有権の残数をインクリメントする関数マクロ)。

所有権の発行とは「所有権の獲得」を行う1つの手段となります(他の手段については 参照の所有権の獲得 で解説します)。

それに対し、Py_DECREF引数で指定された参照の所有権を破棄する関数マクロ(所有権の残数をデクリメントする関数マクロ)と考えることができます。

所有権の破棄とは「所有権の放棄」を行う1つの手段となります(他の手段については 参照の所有権の放棄 で解説します)。

所有権による放棄を行う関数の明確化

所有権のルールに基づいて実装を行うことで、参照の所有権の放棄を行う関数の明確化を行うことができます。

例えば下の図は、オブジェクトへの参照の所有権を持つ関数 A から、引数に「オブジェクト obj1 への参照(ポインタ変数 x)」を指定して関数 B を呼び出す際の参照や所有権の様子を示す図になります。

関数呼び出し時の所有権と参照の関係を示す図

基本的には、関数呼び出し時には参照の所有権は移動しません。

なので、obj1 への参照は関数 A と関数 B の両方が持っていることになりますが、obj1 への参照の所有権を持っているのは関数 A のみとなります。そのため、obj1 の参照カウント(所有権の残数)は 1 ということになります。

関数 Aobj1 への参照の所有権を持っているため、その参照の所有権を放棄することができますが、関数 Bobj1 への参照の所有権を持っていないため、その参照の所有権を放棄することはできません。

つまり、関数 B は関数 A から受け取った参照に対して Py_DECREF を実行してはいけないことになります(Py_DECREF は所有権を破棄する関数マクロであり、所有権の破棄は所有権の放棄の1つの手段です)。

また、オブジェクトは Py_DECREF の中で参照カウントが 0 になった際に解放されます。ですので、関数 B が関数 A から受け取った参照に対して Py_DECREF を実行できないということは、関数 B が終了して関数 A に処理が戻ってきた際には、必ず obj1 は存在しているということになります。

なので、関数 A は関数 B 実行後も安心して obj1 を使用することができます。所有権のルールが守られていれば、関数は、参照の所有権を保持している限り、その参照の参照先のオブジェクトが必ず存在するという前提で処理を行うことができます。

関数呼び出し後に必ずオブジェクトが残っていることを示す図

さらに、関数 B は所有権を持っていないのですから、関数 B に関しては所有権を放棄することを考慮せずに実装することができます(少なくとも関数 A から受け取った参照に関しては)。

関数Bがobj1への参照の所有権を放棄する必要がないことを示す図

つまり、所有権のルールに従えば、関数は所持している参照の所有権の放棄さえ行えば良いということになります(Py_DECREF の実行など)。

なので、Py_INCREF と Py_DECREF の実行タイミング で説明したような、参照が不要になった際 or 参照がなくなる際に毎回 Py_DECREF を実行するという面倒なことはしなくても良いのです。

このように、所有権を放棄できる関数を明確化しておくことで、意図しないタイミングでのオブジェクトの解放を防いだり、プログラマーの実装の負担を軽減したりすることが可能になります。

ルールを破ることの危険性

ただし、所有権のルールを破ってしまうと、オブジェクトの解放漏れや解放済みのメモリへのアクセス等が発生する可能性があるので注意してください。

例えば先程の例で考えれば、関数 Bobj1 への参照の所有権を持っていないにも関わらず Py_DECREF を実行して所有権を放棄してしまうと、obj1 の参照カウントが 0 になって obj1 が解放されてしまいます(実行した関数が参照の所有権を保持しているかどうかに関わらず、Py_DECREF はできてしまうのです…)。

持っていない所有権を破棄する様子

ですが、関数 A は自身が obj1 への参照の所有権を持っているのですから、関数 Bobj1 の解放が行われているとは夢にも思いません。なので、当然関数 B 実行後にも obj1 を利用する可能性があります。

そうなると関数 A は解放済みのオブジェクトにアクセスすることになり、メモリアクセス違反が発生することになります。

所有権のルールが破られてメモリアクセス違反が発生する様子

逆に、関数 B がルールを守って所有権の放棄を行わなかった場合でも、もし関数 A が義務を怠ってオブジェクトの参照の所有権を放棄せずに関数を終了してしまうと、所有権を持つ関数が存在しないにもかかわらずオブジェクトが残ってしまうことになります。

前述の通り、参照の所有権を持つ関数のみが所有権の放棄を行えるわけですので、所有権を持つ関数が存在しなくなると、その参照に対して Py_DECREF が実行可能な関数が存在しなくなることになります。

つまり、このオブジェクトはプログラムが終了するまで残り続けることになり、オブジェクトの解放漏れ(メモリリーク)が発生することになります。

所有権のルールが破られてメモリリークが発生する様子

このように、所有権のルールを破ってしまうと、一気にプログラムの動作が不安定になります。

なので、オブジェクトの参照を行う関数を実装する際には、その関数がどの参照の所有権を持ち、どの参照の所有権を持っていないかをしっかり意識しながら実装していく必要があります。

所有権の移動

また、所有権は関数間で移動することもあるので注意してください。

所有権の移動の一例として、関数からの参照の返却が挙げられます。関数から呼び出し元に対してオブジェクトへの参照を返却する場合、所有権も含めて返却を行うことができます。

例えば、関数 A から関数 B を呼び出し、

関数Aが関数Bを呼び出す様子

次に関数 B 内部で obj1 を生成し、さらに obj1 への参照の所有権を発行して獲得したとします(Py_INCREF の実行)。

関数Bがobj1への参照の所有権を獲得する様子

さらに関数 B が、その obj1 への参照を return で返却した場合、関数 B は終了し、呼び出し元の関数 A は参照と一緒にその参照の所有権も受け取ることになります。つまり関数 B から関数 Aobj1 への参照の所有権が移動し、関数 A にその所有権を放棄する義務が発生することになります。

関数Bから関数Aに所有権が移動する様子

このように、所有権は関数間で移動することがありますので、特に関数呼び出し時は所有権の移動が行われるかどうかをしっかり意識して実装を行う必要があります。

ちなみにですが、上記の例では関数 B は所有権を所持しているのにも関わらず、所有権を破棄せずに(Py_DECREF を実行せずに)関数を終了していることになります。この場合、関数 B は所有権を所持しているために発生する義務を怠ったことにはならないのでしょうか?

答えは「ならない」です。所有権を所持することで発生する義務は、その所有権を「放棄する」ことであり、Py_DECREF による所有権の破棄は放棄の手段のうちの1つでしかありません。

上記の場合は、「参照の所有権を委譲する」ことで所有権の放棄を行なっています。なので、関数 B は義務をしっかり果たして終了しているので問題ありません。

スポンサーリンク

参照の所有権の獲得

なんとなく所有権がどういうものかイメージが湧いてきたでしょうか?

難しそうですが、要は所有権のルールを守るために所有権の獲得 or 所有権の放棄を適切に行えば良いだけです。

では、どうすれば所有権を獲得することができ、どうすれば所有権を放棄することができるのか、この点について解説していきたいと思います。

まずは所有権の獲得について解説していきます。

Py_INCREF を実行して所有権を発行する

関数が参照の所有権を獲得する一番分かりやすい手段は Py_INCREF の実行になります。

オブジェクトへの参照を引数に指定して Py_INCREF を実行することで、そのオブジェクトの所有権を発行して獲得することができます。

オブジェクトへの参照の所有権を発行する様子

例えば下記を実行すれば、ポインタ x は  obj1 への参照となります。が、下記を実行しただけでは obj1 への参照が得られるだけであり、所有権は得られません。

オブジェクトの参照
/* obj1はPyObject型の変数 */
PyObject *x = &obj1;

ですが、下記のように Py_INCREF(x) を行えば、実行した関数は obj1 への参照の所有権を発行して獲得することができます。

参照の所有権の獲得
/* obj1はPyObject型の変数 */
PyObject *x = &obj1;

Py_INCREF(x); /* 所有権の獲得 */

参照の所有権の発行は、その参照の参照先のオブジェクトの参照カウントを増加させる唯一の手段となります。

他の関数から所有権を委譲してもらう

また、参照の所有権は他の関数から委譲してもらうことが可能です。

Python/C API から所有権を委譲してもらえる

Python/C API の中にはオブジェクトへの参照を返却する API が存在します。その API の中には、参照を返却するとともに、その参照の所有権を委譲するものがあります。

例えば、Python/C API の1つに PySequence_GetItem という API が存在します。この API は、リストやタプル等のシーケンスオブジェクトからオブジェクトを取得する API になります(リストの中から要素を取得する感じの API)。

この API の返却値の型は PyObject * であり、つまりはオブジェクトへの参照を返却する API になります。そして、この API はその参照の所有権を呼び出し側に委譲します。

PySequence_GetItemからオブジェクトへの参照の所有権を委譲してもらう様子

それに対し、PyList_GetItem というリストオブジェクトからオブジェクトを取得する API においては、API の返却値の型は PyObject * でオブジェクトへの参照を返却する API ではあるものの、参照の所有権の委譲は行いません。

PyList_GetItemからオブジェクトへの参照のみを受け取る様子

前述の通り、参照の所有権を持っていない関数は、その参照の所有権を放棄することができません。また、参照の所有権を放棄する1つの方法は Py_DECREF の実行となります(その他の所有権の放棄については後述で解説します)。

そのため、呼び出し側の関数は、PySequence_GetItem から返却された参照に対して Py_DECREF は実行しても良いものの、PyList_GetItem から返却された参照に対しては Py_DECREF を実行してはいけません。

例えば下記は所有権を持つ参照に対して所有権の放棄を行なっているので問題ないですが、

所有権を持つ参照の所有権の放棄
/* seqはオブジェクト */
PyObject *item = PySequence_GetItem(seq, 0);

Py_DECREF(item); /* OK */

下記に関しては所有権を持っていない参照に対して所有権の放棄を行なっており、オブジェクトの二重解放等が発生する可能性があります。

所有権を持たない参照の所有権の放棄
/* listはオブジェクト */
PyObject *item = PyList_GetItem(list, 0);

Py_DECREF(item); /* NG!!! */

このように、関数や API を実行してオブジェクトへの参照を取得する場合、その参照の所有権の委譲が行われているかどうかを考慮して実装する必要があります。

Python/C API の所有権の委譲の有無の確認

なので、特にオブジェクトの参照を取得する関数や API に関しては、それらの関数や API が所有権を委譲するのかどうかをしっかり理解した上で利用する必要があります。

特に Python/C API を利用してオブジェクトの参照を取得する場合に関しては、下記の「Python/C API リファレンスマニュアル」が役に立つと思います。

https://docs.python.org/ja/3/c-api/index.html

このマニュアルでは各 API の仕様が紹介されており、その API がオブジェクトへの参照を返却するものである場合(つまり返却値の型が PyObject * である場合)、返却値が New reference or Borrowed reference のどちらであるかが明記されています。

返却値が New reference である場合、API からオブジェクトへの参照が返却されるとともに、その参照の所有権が委譲されることになります。

PySequence_GetItemの関数仕様

引用元:シーケンス型プロトコル

それに対し、返却値が Borrowed reference である場合、API からオブジェクトへの参照が返却されますが、その参照の所有権は委譲されません。

PyList_GetItemの関数仕様

引用元:リストオブジェクト

こんな感じで、マニュアルから所有権が委譲されるかどうかを確認することができますので、オブジェクトへの参照が返却される Python/C API を利用する場合は、是非上記ページを参考にしてください。

引数での参照の受け渡しでは所有権は委譲されない

また、関数は引数で参照を受け取ることができますが、この引数での参照の受け渡しにおいては所有権は委譲されないので注意してください。

関数が引数で受け取った参照を放棄した場合、それは後述の 所有権の盗み取り で解説する通り、関数呼び出し元の所持する所有権を盗み取ることになります。

関数呼び出し元の動作に影響を及ぼすことになるため、引数で受け取った参照は放棄しない、もしくはあらかじめ所有権を獲得した上で放棄を行うようにしてください。

もし所有権の盗み取りを行う関数を作成するのであれば、それが関数利用者に分かるよう、盗み取ることを関数の仕様としてしっかり明記しておく必要があります。

スポンサーリンク

保存されている所有権を取得する

他にも、グローバル変数や static 変数等に保存されている所有権を取得して獲得することもできます。

これに関しては、放棄の解説と一緒に読んだ方が分かりやすいかと思いますので、後述の グローバル変数等に所有権を保存する で一緒に解説させていただきたいと思います。

参照の所有権の放棄

参照の所有権を獲得した関数は、関数が終了するまでに参照の所有権を放棄する必要があります。

ご存知の通り、ローカル変数は関数が終了すると解放されてしまいます。これは参照(PyObject * 型のローカル変数)の場合も同様です。ですが、参照は解放されたとしても、その参照の所有権は破棄されずに残り続けます。

参照は解放されているため、所有権のみが宙ぶらりんの状態で残り続けている感じです。

さらに、参照の所有権を破棄する際にはその参照が必要となります(参照を引数に指定して Py_DECREF を実行する必要がある)。なので、参照のみが解放されて宙ぶらりんの状態になってしまった所有権は、もはや破棄することができません。

所有権を保持したまま関数が終了してしまう様子

さらに、オブジェクトの参照カウントは、そのオブジェクトへの参照の所有権の残数と考えることができますので、所有権が残り続けている間、オブジェクトも解放されることはありません。つまり、オブジェクトの解放漏れ(メモリリーク)が発生することになります。

こうならないためにも、所有権を獲得した関数はその所有権を関数が終了するまでに放棄する必要があります。

続いては、この参照の所有権を放棄の仕方について確認していきたいと思います。

Py_DECREF を実行して所有権を破棄する

ここまでの説明でも何度か登場しましたが、参照の所有権は、その参照を引数に指定して Py_DECREF を実行することで放棄することができます。

オブジェクトへの参照の所有権を破棄する様子

Py_INCREF を参照の所有権を発行する関数と考えると、Py_DECREF は参照の所有権を破棄する関数と考えることができます。そして、この破棄によりオブジェクトへの参照の所有権の残数(参照カウント)が減ることになります。

そして、参照カウントが 0 になった際には、そのオブジェクトが解放されることになります。

参照の所有権の破棄は、その参照の参照先のオブジェクトの参照カウントを減少させる唯一の手段となります。

スポンサーリンク

他の関数に所有権を委譲する

また、他の関数に委譲することで所有権を放棄することもできます。

所有権を委譲した場合、所有権を受け取った関数側に参照の所有権を放棄する義務が発生します。

所有権は、関数が所有権を持つ参照を return で返却することにより呼び出し元の関数に委譲することができます。

オブジェクトへの参照の所有権を委譲する様子

ちょうどこれは、他の関数から所有権を委譲してもらう で紹介した Python/C API のマニュアルにおける、返却値が New reference である API と同様の返却の仕方となります。

重要なのは、あくまでも委譲できるのは「所有権を持つ参照を return した時のみ」である点になります。

当然、持っていない所有権は委譲することはできません。

もし所有権を持っていない参照の所有権を他の関数に委譲をしたいのであれば、その所有権を獲得してから return で参照を返却する必要があります。

具体的には、Py_INCREF を実行して所有権を発行して獲得し、

委譲を行うために所持していない所有権を発行して獲得する様子

それから所有権の委譲を行うことになります。

発行した所有権を他の関数に委譲する様子

また、所有権を持っていない参照を return した場合、参照自体は呼び出し元に渡すことはできますが、所有権は委譲されません。

参照のみを他の関数に返却する様子

ちょうどこれは、他の関数から所有権を委譲してもらう で紹介した Python/C API のマニュアルにおける、返却値が Borrowed reference である API と同様の返却の仕方となります。

この場合、関数呼び出し元は参照の所有権は獲得できないことになるため、参照の所有権の放棄を行なってはいけません。

グローバル変数等に所有権を保存する

また、参照の所有権は保存することで放棄することも可能です。

参照の所有権の保存とは、グローバル変数や static 変数等の関数が終了されても保持される変数に所有権を持つ参照を格納しておくことを言います。この格納により、参照に付随する形で所有権もグローバル変数に保存されます。

格納するのはオブジェクトへの参照となりますので、グローバル変数の型は PyObject * である必要があります。

所有権を持つ参照をグローバル変数格納して所有権を保存する様子

この保存により、関数 A は所有権を放棄したことになるため、他の手段で所有権を放棄しなくても関数を終了することができるようになります。

例えば、オブジェクトへの参照の所有権の残数が 1 の場合、Py_DECREF を実行してしまうとオブジェクトが解放されてしまうことになりますが、保存によって所有権を放棄した場合は、オブジェクトを残したまま&他の関数への所有権を委譲せずに関数を終了することができます。

これにより、関数 A 終了後も他の関数が実行された時に、もしくは関数 A 自身が再度実行された時に、グローバル変数からオブジェクトへの参照の所有権を獲得することが可能になります。

保存された所有権を獲得する様子

ただ、このグローバル変数に所有権を保存すると、所有権の所在が曖昧になりがちになるので注意してください。

考え方としては、グローバル変数に保存した所有権は、そのグローバル変数を利用した関数に即座に移動することになると考えるのが安全だと思います。

つまり、上の図で考えれば、グローバル変数 g から参照を取得するときはもちろん、グローバル変数 g に別の参照を格納する際にも所有権は関数 B に移動することになります。

特に後者の場合、所有権のルールを守るのがちょっと大変になります。

グローバル変数 g に直接別の参照を格納すると、グローバル変数 g に保存されていた所有権は関数 B に移動することになります。ですが、グローバル変数 g に直接別の参照を格納した時点で、元々格納されていた参照は上書きされて消えてなくなることになり、その参照をもう取得できなくなってしまいます。

参照が取得できないと、所有権は放棄することができません。つまり関数 B は所有権のルールを破ることになり、この場合はメモリリークが発生することになります。

保存していた所有権の放棄ができなくてメモリリークが発生する様子

この場合、所有権のルールを守るためには、関数 B はあらかじめグローバル変数 g に保存されている所有権を放棄してから、別の参照をグローバル変数 g に格納する必要があることになります。

所有権を確実に放棄するための保存した所有権を扱う際の処理の流れ

ソースコードとしては下記のようなイメージになると思います。グローバル変数に NULL が格納されている可能性があるため、Py_XDECREF を利用しています。

保存した所有権の扱い
PyObject *g = NULL;

void
func1(PyObject *z) {

    /* 保存していた所有権を取得 */
    PyObject *y = g;

    /* 所有権を放棄 */
    Py_XDECREF(y);

    /* 所有権を発行 */
    Py_INCREF(z);

    /* 所有権を保存 */
    g = z;
}

一旦 yg の参照を受け取る形にしていますが、直接 Py_XDECREF(g) でグローバル変数に保存されている所有権を放棄しても良いです。

下記のように記述するとメモリリークが発生するので注意してください。

誤った保存した所有権の扱い
PyObject *g = NULL;

void
func1(PyObject *z) {

    /* 所有権を発行 */
    Py_INCREF(z);

    /* 所有権を保存 */
    g = z;
    
    /* gに格納されていた参照が取得できない */
}

このように、グローバル変数に保存した所有権はそのグローバル変数を利用した関数に即座に移動することになると考え、さらに所有権のルールを守るためにはどういう順番で処理を行えば良いかを意識しながら実装するようにすれば、安全なプログラム(メモリリークが発生しないプログラム)を実現しやすくなると思います。

いずれにせよ、保存による所有権の放棄は結構ややこしいと思いますので、不要であれば保存はしない方が良いと思いますし、保存せざるを得ない場合は特に所有権を意識しながら実装するのが良いと思います。

所有権の盗み取り

関数が所有権を放棄する手段は、ここまで紹介してきた破棄・委譲・保存の3パターンになります。

受動的な所有権の放棄

が、所有権は他の関数から盗み取られる可能性があり、これによって所有権を失うことがあるので注意してください。

破棄や委譲・保存に関しては、所有権を持つ関数自らが能動的に所有権の放棄を行いますが、盗み取りの場合は他の関数によって受動的に所有権の放棄を行うことになります。

MEMO

下記の Python/C API リファレンスマニュアルにおいては、所有権の盗み取りではなく “参照の盗み取り” という単語が使われているので注意してください

用語としては当然公式のリファレンスマニュアルの方が正しいです

が、ここまでの解説の流れからすると “所有権の盗み取り” と言った方が分かりやすいかと考え、所有権の盗み取りという用語を利用しています

いずれにしても、関数実行によって所有権を失うという点では同じです

https://docs.python.org/ja/3/c-api/index.html

といっても、所有権の盗み取りが発生するのは特定の関数や API を呼び出した時のみであり、盗み取りされる所有権は「呼び出し時に引数に指定した参照」の所有権のみとなります。

関数Aの持つオブジェクトへの参照の所有権が関数Bに盗み取りされる様子

所有権を盗み取る Python/C API の1つが PyList_SetItem になります。

所有権を盗み取るAPI
int PyList_SetItem(PyObject *list, Py_ssize_t index, PyObject *item)

PyList_SetItem は、リストオブジェクトにオブジェクトを要素として挿入する API になります。挿入したいオブジェクトへの参照を引数 item に指定して実行することになるのですが、この際に item の所有権が PyList_SetItem に盗み取られることになります。

そのため、PyList_SetItem 実行することで、関数は item に指定した参照の所有権を失うことになります。

盗み取りに関する注意点

この所有権の盗み取りに関する注意点について説明しておきます。

まず、所有権の盗み取りを行う関数や API を実行した後は、呼び出し側の関数は盗み取られた参照の所有権を放棄してはいけません。所有権はすでに失っています。

例えば、PyList_SetItem 実行後、引数 item に指定した参照の所有権は放棄してはいけません。

また、所有権の盗み取りを行う関数や API を実行する際、呼び出し側の関数は盗み取られる参照の所有権を所持しておく必要があります。盗み取りを行う関数は呼び出し側から参照の所有権を盗み取ることができることを前提に動作します。

例えば、PyList_SetItem 実行する際には、関数は引数 item に指定する参照の所有権を所持しておく必要があります。もし所有権を保持していない参照を item に指定する場合、事前に Py_INCREF を実行して所有権を獲得しておく必要があります。

Python/C API の場合、所有権の盗み取りを行う場合はその旨が API 仕様に明記されています(参照を盗み取ると記述されている)。

PyList_GetItemの仕様

引用元:リストオブジェクト

所有権の盗み取りを行う Python/C API は稀です。なので、基本的には Python/C API の引数に参照を指定する場合は所有権の盗み取りは発生しないと考えて良いです。

ですが、実行する API がたまたま所有権の盗み取りをする API である可能性ももちろんありますので、初めて使用する API に関しては Python/C API リファレンスを参照して API 仕様を確認する方が良いと思います。

スポンサーリンク

所有権を扱う際のポイント

ここまで所有権について、さらに所有権の獲得と所有権の放棄について解説してきました。

ここで、ここまでのおさらいの意味も含め、所有権を扱う際のポイントを挙げていきたいと思います。

まず、所有権を扱う上での最大のポイントは下記の2つです。

  • 所有権のルールを守る
  • 所有権を所持しているかどうかを把握する

一番大事なのはルールを守ること

まず所有権のルールを守って実装することが大前提となります。このルールを破るとメモリリークやメモリアクセス違反等が発生します。

また、所有権のルールを守ることを意識して実装していれば、Py_INCREF や Py_DECREF を実行すべきタイミングは自然と浮き彫りになってくるはずです。

他に所有権を獲得する手段がない時に Py_INCREF を実行する

Py_INCREF に関しては、基本的には「放棄を行いたいけど他に所有権を獲得する手段がない時」が実行すべきタイミングであると考えて良いと思います。

例えば、呼び出し元の関数に所有権を委譲したい場合、ルールを守るためには事前に返却する参照の所有権を獲得しておく必要があります。

もし、その所有権が他の関数から委譲されない&グローバル変数に保存されていないのであれば、もう Py_INCREF を実行するしかないです。

基本は「放棄を行いたいけど他に所有権を獲得する手段がない時」が Py_INCREF を実行すべきタイミングとなりますが、一部の例外となるケースでも実行が必要になることがあるので注意してください。

この例外となるケースは下記ページの 薄氷 という章で解説されています。

https://docs.python.org/ja/3/extending/extending.html

他に所有権を放棄する手段がない時に Py_DECREF を実行する

また、Py_DECREF に関しては、「不要になった&放棄を行わなければならないけど他に所有権を放棄する手段がない時」が実行すべきタイミングであると考えて良いと思います。

例えば、返却値なしの関数で、さらにグローバル変数を利用していないような場合、ルールを破らないためには Py_DECREF を実行して所有権を破棄するしかないです。

こんな感じで、各関数でやりたいことを実現する際に、ルールを守るためにはどうすれば良いかを考えていけば、自然と Py_INCREFPy_DECREF を行うタイミングが浮き彫りになるはずです。

ルールを守るために関数や API の仕様の理解が大事

ですが、ルールを守るためには、まず関数の各処理のタイミングでオブジェクトへの参照の所有権を所持しているかどうかを把握しながら実装を行う必要があります。

特に関数や API を呼び出して参照を返却値として受け取る場合、所有権が委譲される場合もあれば委譲されない場合もあります。これらによって関数や API 実行後に呼び出し側の関数が受け取った参照の所有権を所持しているかどうかが変わります。

また、引数で指定した参照の所有権を盗み取りする関数や API もあります。

なので、参照の所有権を所持しているかどうかを把握しながら実装するためには、実行する関数や API の仕様を知っていなければなりません(所有権が委譲されるかどうか、所有権を盗み取りするかどうか)。

特に Python/C API の場合は、下記の Python/C API リファレンスマニュアルで各 API 仕様が紹介されており、所有権の委譲や盗み取りに関する情報も明記されていますので、Python/C API を利用する場合は、利用する API の仕様に必ず目を通した方が良いです。

https://docs.python.org/ja/3/c-api/index.html

参照の所有権が委譲されるかどうかは、返却値が New reference or Borrowed reference のどちらであるかで判断することが出来ます。前者の場合は所有権が委譲され、後者の場合は所有権は委譲されません。

所有権の盗み取りが行われるかどうかは、仕様の説明の中に「参照の盗み取りを行う」といった記述があるかどうかで判断することができます(所有権の盗み取りでなく “参照の盗み取り” と記述されているので注意してください)。

同様に、自身が Python オブジェクトを利用する関数を作成し、その関数を他の人に利用してもらうような場合は、関数仕様として所有権の扱いについても明記しておくようにしましょう。この記載がないと、関数利用者は関数実行後に所有権を所持しているのかがどうかが把握できなくなってしまいます。

また、呼び出し先の関数だけではなく、呼び出し元が期待していることを理解することも重要になります(所有権の委譲を期待しているのかどうか)。

例えば、Python スクリプトから呼び出されるラッパー関数では、必ず Python スクリプト側に対して返却する参照の所有権を委譲しなければなりません。

これは、Python スクリプト側が呼び出した関数から参照の所有権が委譲されてくることを期待して動作しているからです。

スポンサーリンク

理解度チェック

最後に、実際にソースコードを確認しながら、Py_INCREFPy_DECREF を実行すべきタイミングを考えてみましょう!

ここから紹介する関数は全て、Python スクリプトから呼び出し可能な関数をC言語で実装した例となります。

Python スクリプトから呼び出すための手順は下記ページで解説していますので、実際に動作させてみたい場合は下記ページを参考にしてください(これから紹介する関数は、下記ページにおけるラッパー関数の位置付けになります)。

PythonからC言語の関数を呼び出す方法の解説ページアイキャッチ PythonからC言語の関数を呼び出す(基本編)

また、ここから紹介する関数は全て Py_INCREF や Py_DECREF の実行の仕方に問題があります。実行不要なタイミングで Py_INCREF や Py_DECREF を実行したり、必要な Py_INCREF や Py_DECREF の実行が抜けていたりします。なので、全ての関数はルールを破っており、実行するとメモリリークやメモリアクセス違反等が発生します。

関数の実装を確認し、所有権のルールを守るためにどう修正する必要があるかを是非考えてみてください。ちなみに理解度チェック1 〜 理解度チェック5においてはグローバル変数を利用しないことを前提に問題を作っています。

Python/C API を利用していますので、知らない API が登場した場合はその API の仕様を是非調べてみてください。おそらく API 名でググれば検索結果の一番上にその API の仕様が紹介されているページが見つかるはずです。

理解度チェック1

では理解度チェックの1問目です。

問題

下記関数は、関数呼び出し元に対して None オブジェクトへの参照の所有権を委譲する関数です。Py_NoneNone オブジェクトへの参照となります。

この関数のどこに問題があるでしょうか?

理解度チェック1の問題
#define PY_SSIZE_T_CLEAN
#include <Python.h>

static PyObject *
_func(PyObject *self, PyObject *args) {

    PyObject *ret = Py_None;
    return ret;
}

ちなみに、上記関数を Python スクリプトの無限ループの中で実行すると、いずれ下記のエラーメッセージが発生することになります。

Fatal Python error: deallocating None

解答

最初のチェックにしてはちょっと特殊だったかもしれないですね…。Python では None も他のオブジェクト同様に扱う必要が有ります。

まず、関数は None への参照の所有権を委譲して放棄するのですから、関数は委譲する際に None への参照の所有権を所持しておく必要が有ります。

ret = Py_None を実行しても、単に ret が None への参照となるだけであり、None への参照の所有権が得られるというわけでは有りません。

そのため、return ret を実行しても、関数呼び出し元に None への参照の所有権を委譲することができません。

None への参照の所有権を委譲するためには、まず None への参照である ret に対して Py_INCREF を実行する必要があります。そして、これによって None への参照の所有権を獲得し、それから None への参照 ret を return して所有権を委譲する必要が有ります。

理解度チェック1の解答
#define PY_SSIZE_T_CLEAN
#include <Python.h>

static PyObject *
_func(PyObject *self, PyObject *args) {

    PyObject *ret = Py_None;

    Py_INCREF(ret);
    return ret;
}

理解度チェック2

次は理解度チェックの2問目です。

問題

下記は Python スクリプトからリストを受け取り、リストの先頭要素を表示する関数となります。返却値は None への参照となります(所有権も委譲)。

この関数の問題点は何でしょうか?

Python/C API を利用していますので、その API から所有権が委譲されるかどうかをしっかり確認しながら考えてみてください。

理解度チェック2の問題
#define PY_SSIZE_T_CLEAN
#include <Python.h>

static PyObject *
_func(PyObject *self, PyObject *args) {

    PyObject *list;

    if (!PyArg_ParseTuple(args, "O", &list)) {
        return NULL;
    }

    PyObject * item = PyList_GetItem(list, 0);
    long value = PyLong_AsLong(item);
    Py_DECREF(item);

    printf("%ld,", value);

    PyObject *ret = Py_None;
    Py_INCREF(ret);
    return ret;
}

解答

この _func 関数では Py_DECREF を除いて次の3つの Python/C API を利用しています。

  • PyArg_ParseTuple
  • PyList_GetItem
  • PyLong_AsLong

PyArg_ParseTuple は第3引数以降の引数でオブジェクトへの参照を取得する API になりますが、所有権の委譲は行われません。

つまり、_func 関数は PyArg_ParseTuple 実行後、オブジェクトへの参照として list が得られますが、その所有権は持たないことになります。

PyList_GetItem は返却値でオブジェクトへの参照を返却する API になりますが、これも所有権の委譲は行われません。

つまり、_func 関数は PyList_GetItem 実行後、オブジェクトへの参照として item が得られますが、その所有権は持たないことになります。

PyLong_AsLong に関しては、そもそもオブジェクトへの参照を取得する API ではありません。

すなわち、_func 関数は Python/C API を実行しても、オブジェクトへの参照の所有権を得ることはありません。

にも関わらず、_func 関数は PyList_GetItem から得られた item に対して Py_DECREF を実行して所有権を放棄してしまっているので、所有権のルールを破っていることになります。

なので、Py_DECREF(item) の実行をしている点が上記の _func 関数の誤りであり、次のように修正する必要があります。

理解度チェック2の問題
#define PY_SSIZE_T_CLEAN
#include <Python.h>

static PyObject *
_func(PyObject *self, PyObject *args) {

    PyObject *list;

    if (!PyArg_ParseTuple(args, "O", &list)) {
        return NULL;
    }

    PyObject * item = PyList_GetItem(list, 0);
    long value = PyLong_AsLong(item);

    printf("%ld,", value);

    PyObject *ret = Py_None;
    Py_INCREF(ret);
    return ret;
}

None への参照 ret の返却の仕方は、理解度チェック1 で解説した通り上記で問題ありません。

スポンサーリンク

理解度チェック3

次が理解度チェックの3問目となります。

問題

理解度チェック2 同様、下記は Python スクリプトからリストを受け取り、リストの先頭要素を表示する関数となります。返却値は None への参照となります(所有権も委譲)。

ただ、理解度チェック2 の時とは使用している Python/C API が違います。その違いに注目して問題点について考えてみてください。

理解度チェック3の問題
#define PY_SSIZE_T_CLEAN
#include <Python.h>

static PyObject *
_func(PyObject *self, PyObject *args) {

    PyObject *list;

    if (!PyArg_ParseTuple(args, "O", &list)) {
        return NULL;
    }

    PyObject * item = PySequence_GetItem(list, 0);
    long value = PyLong_AsLong(item);

    printf("%ld,", value);

    PyObject *ret = Py_None;
    Py_INCREF(ret);
    return ret;
}

解答

使用している Python/C API に関して言えば、理解度チェック2_func 関数と今回の _func 関数との違いは下記のみになります。

  • PyList_GetItem から PySequence_GetItem に変更

この2つにおいて、所有権に関して下記の違いがあります。

  • PyList_GetItem:返却する参照の所有権は委譲しない
  • PySequence_GetItem:返却する参照の所有権を委譲する

そのため、PySequence_GetItem から返却された参照の所有権は、_func 関数が終了するまでに放棄する必要があります。

にも関わらず、_func 関数は PySequence_GetItem から返却された参照の所有権の放棄を行なっていません。

_func 関数の返却値は None への参照なので、他の参照の所有権の委譲を行うことはできません。そのため、_func 関数は関数終了までに item に対して Py_DECREF を実行して所有権を破棄する必要が有ります。

item が不要になるのは PyLong_AsLong 実行後ですので、下記のように Py_DECREF は下記のように PyLong_AsLong 実行後 〜 関数終了の間に実行する必要があります。

理解度チェック3の解答
#define PY_SSIZE_T_CLEAN
#include <Python.h>

static PyObject *
_func(PyObject *self, PyObject *args) {

    PyObject *list;

    if (!PyArg_ParseTuple(args, "O", &list)) {
        return NULL;
    }

    PyObject * item = PySequence_GetItem(list, 0);
    long value = PyLong_AsLong(item);
    Py_DECREF(item);

    printf("%ld,", value);

    PyObject *ret = Py_None;
    Py_INCREF(ret);
    return ret;
}

理解度チェック4

続いて4問目となります。

ちょっと所有権の考え方に慣れてきたでしょうか?

問題

下記は Python スクリプトから3つのオブジェクトを受け取り、それらを格納したリストを返却する関数となります(所有権も委譲)。

この関数のどこに問題があるでしょうか?

理解度チェック4の問題
#define PY_SSIZE_T_CLEAN
#include <Python.h>

static PyObject *
_func(PyObject *self, PyObject *args) {

    PyObject *obj1, *obj2, *obj3;

    if (!PyArg_ParseTuple(args, "OOO", &obj1, &obj2, &obj3)) {
        return NULL;
    }

    PyObject *list = PyList_New(3);

    PyList_SetItem(list, 0, obj1);
    PyList_SetItem(list, 1, obj2);
    PyList_SetItem(list, 2, obj3);

    return list;
}

解答

まず、PyList_New は返却する参照の所有権を委譲しますので、最後にreturn で所有権を委譲して放棄する点は正しいです。

ただ、PyList_SetItem は第3引数で指定した参照の所有権を盗み取りします。なので、第3引数で指定した参照の所有権を獲得してから PyList_SetItem を実行する必要があります。

理解度チェック1 でも解説したように PyArg_ParseTuple では引数から参照を取得することができますが、所有権は委譲されません。

そのため、PyList_SetItem の第3引数に指定する参照の所有権を Py_INCREF で獲得しておく必要があります。

理解度チェック4の解答
#define PY_SSIZE_T_CLEAN
#include <Python.h>

static PyObject *
_func(PyObject *self, PyObject *args) {

    PyObject *obj1, *obj2, *obj3;

    if (!PyArg_ParseTuple(args, "OOO", &obj1, &obj2, &obj3)) {
        return NULL;
    }

    PyObject *list = PyList_New(3);

    Py_INCREF(obj1);
    Py_INCREF(obj2);
    Py_INCREF(obj3);

    PyList_SetItem(list, 0, obj1);
    PyList_SetItem(list, 1, obj2);
    PyList_SetItem(list, 2, obj3);

    return list;
}

理解度チェック5

次が5問目です。残り2問です!

問題

理解度チェック4 同様に、下記は Python スクリプトから3つのオブジェクトを受け取り、それらを格納したリストを返却する関数となります(所有権も委譲)。

ただし、理解度チェック4 の時とは実現の仕方が異なっており、下記の関数の実装には問題があります。どこに問題があるでしょうか?

理解度チェック5の問題
#define PY_SSIZE_T_CLEAN
#include <Python.h>

static PyObject *
_func(PyObject *self, PyObject *args) {

    double val1, val2, val3;

    if (!PyArg_ParseTuple(args, "ddd", &val1, &val2, &val3)) {
        return NULL;
    }

    PyObject *list = PyList_New(3);

    PyObject *obj1 = PyLong_FromLong(val1);
    PyObject *obj2 = PyLong_FromLong(val2);
    PyObject *obj3 = PyLong_FromLong(val3);

    Py_INCREF(obj1);
    Py_INCREF(obj2);
    Py_INCREF(obj3);

    PyList_SetItem(list, 0, obj1);
    PyList_SetItem(list, 1, obj2);
    PyList_SetItem(list, 2, obj3);

    return list;
}

解答

理解度チェック4 とは異なり、今回は PyArg_ParseTuple でオブジェクトを取得するのではなく、double 型の値を取得するようにしています。

そして、その double 型の値を PyLong_FromLong でオブジェクトに変換して返却値として参照を受け取るようにしています。

PyLong_FromLong では返却する参照の所有権を委譲するため、PyLong_FromLong 実行後に _func 関数はその参照の所有権は所持していることになります。

それにも関わらず、さらに Py_INCREF を実行しているため、各参照の所有権を2つずつ所持することになります。

その後、PyList_SetItem から各参照の所有権は1つずつ盗み取りされることになりますが、それでも所有権を1つずつ所持したまま関数を終了することになります。所有権を所持したまま関数を終了してしまっているので所有権のルールを破ることになり、これによってメモリリークが発生します。

この場合は、Py_INCREF の実行が不要なため、そこをそのまま削除してやれば良いだけです。

理解度チェック5の解答
#define PY_SSIZE_T_CLEAN
#include <Python.h>

static PyObject *
_func(PyObject *self, PyObject *args) {

    double val1, val2, val3;

    if (!PyArg_ParseTuple(args, "ddd", &val1, &val2, &val3)) {
        return NULL;
    }

    PyObject *list = PyList_New(3);

    PyObject *obj1 = PyLong_FromLong(val1);
    PyObject *obj2 = PyLong_FromLong(val2);
    PyObject *obj3 = PyLong_FromLong(val3);

    PyList_SetItem(list, 0, obj1);
    PyList_SetItem(list, 1, obj2);
    PyList_SetItem(list, 2, obj3);

    return list;
}

スポンサーリンク

理解度チェック6

では最後の理解度チェックです!

問題

この問題のみ、グローバル変数を利用して所有権の保存を行うようにしています。

ただ、とりあえず所有権の保存の例を示すための何の意味もない関数になっているので、その点はご容赦ください…。

下記の _func 関数は、Python スクリプトの呼び出し元から引数として2つの整数を受け取り、_new 関数と _set 関数と _del 関数を実行してから None への参照を返却する関数になります(所有権も委譲)。

ただし、_func 関数を実行するとメモリリークが発生します。

メモリリークを防ぐためにどう修正すれば良いでしょうか?

理解度チェック6の問題
#define PY_SSIZE_T_CLEAN
#include <Python.h>

PyObject *g_ref = NULL;

static PyObject *
_new(long i) {

    PyObject *obj = PyLong_FromLong(i);

    g_ref = obj;

    return obj;
}

static PyObject *
_set(long i) {

    PyObject *obj = PyLong_FromLong(i);

    g_ref = obj;

    return obj;
}

void
_del(void) {

    Py_DECREF(g_ref);
    g_ref = NULL;

}

static PyObject *
_func(PyObject *self, PyObject *args) {

    long val1, val2;

    if (!PyArg_ParseTuple(args, "ll", &val1, &val2)) {
        return NULL;
    }

    _new(val1);
    _set(val2);
    _del();

    Py_INCREF(Py_None);
    return Py_None;
}

解答

上記の _set 関数は、_new 関数で保存された参照の所有権の放棄を行なっていないため、メモリリークが発生することになります(_new 関数での PyLong_FromLong によって生成されたオジェクトが解放されない)。

グローバル変数等に所有権を保存する でも解説したように、オブジェクトへの参照をグローバル変数に格納して利用する場合、グローバル変数を利用する関数にはその参照の所有権が自動的に移動すると考えた方がメモリリークは防ぎやすいと思います。

このように考えれば、_set 関数は g_ref を利用しているのにも関わらず g_ref に保存されている所有権を放棄していないため、所有権の放棄漏れが発生してしまっていることにすぐ気づけます。

なので、_set 関数は g_ref に格納されている参照の所有権を放棄してから、g_ref に新たな参照を格納するよう修正する必要があります。

実際にメモリリークを引き起こすのは _set 関数のみになりますが、_newg_ref を利用しているのですから、_new 関数でも同様の所有権の放棄を行う処理を追加した方が無難です。

ちなみに、g_ref は初期値を NULL としていますので、g_ref に格納された参照の所有権を破棄する際には Py_DECREF ではなく Py_XDECREF を利用する方が良いです。

また、func 関数は _new 関数や _set 関数から返却される参照を受け取っていませんが、これらの関数からは所有権は委譲されない、つまりこれらの関数の返却値は Borrowed reference なので、これによってメモリリークが発生することはありません。

理解度チェック6の解答
#define PY_SSIZE_T_CLEAN
#include <Python.h>

PyObject *g_ref = NULL;

static PyObject *
_new(long i) {

    PyObject *obj = PyLong_FromLong(i);

    PyObject *ref = g_ref;
    Py_XDECREF(ref);

    g_ref = obj;

    return obj;
}

static PyObject *
_set(long i) {

    PyObject *obj = PyLong_FromLong(i);

    PyObject *ref = g_ref;
    Py_XDECREF(ref);

    g_ref = obj;

    return obj;
}

void
_del(void) {

    Py_DECREF(g_ref);
    g_ref = NULL;

}

static PyObject *
_func(PyObject *self, PyObject *args) {

    long val1, val2;

    if (!PyArg_ParseTuple(args, "ll", &val1, &val2)) {
        return NULL;
    }

    _new(val1);
    _set(val2);
    _del();

    Py_INCREF(Py_None);
    return Py_None;
}

まとめ

このページでは、Python/C API 利用時の参照カウントについて、所有権の考え方を中心に解説してきました!

所有権の考え方に則って実装を行うことで、Py_INCREFPy_DECREF の実行箇所を最小限に抑え、かつ、メモリリークやメモリアクセス違反・メモリの二重解放を防ぐことを実現することができます。

基本は、参照の所有権を持つ関数は「その参照の所有権を放棄しなければなない」&参照の所有権を持たない関数は「その参照の所有権を放棄してはいけない」というルールを忠実に守れば良いだけです。

ただし、このルールを守るためには、関数が参照の所有権を所持しているかどうかを実装者が把握できていなければいけません。この所有権を所持しているかどうかを把握するためには、利用する関数や API の仕様を理解しておくことが重要になります。

なので、オブジェクトの参照を扱う関数を実装する際は、使用する関数や API の仕様をより一層しっかり理解することを心がけましょう!

同じカテゴリのページ一覧を表示