このページでは、Python/C API における所有権と参照カウントについて解説していきたいと思います。
所有権については、下記ページで Python 公式から明確な説明が行われています。
ただ、所有権は Python/C API での参照カウントの扱いを理解する上では非常に重要なものですので、もうちょっと砕けた言い方や図などで説明を補うなどして、所有権についてこのページでも解説をしてみることにしました。
この所有権の考え方を理解すれば、Python/C API を利用する際に Py_INCREF
や Py_DECREF
をいつ実行すれば良いかが明確に分かるようになると思います。
Python/C API を利用した Python の拡張や Python からのC言語の関数の呼び出しを行う際には役立つ知識になると思いますので、こういったことに挑戦してみたい方は是非読んでみてください。
ちなみに、「Python からのC言語の関数呼び出し」については下記ページで解説しています。一度 Python/C API を利用したC言語の関数呼び出しを経験しておくと、このページの解説内容がわかりやすくなると思いますので、特に Python/C API を利用したことがない方には事前に下記ページを読んでいただくことをオススメします。
PythonからC言語の関数を呼び出す(基本編)オブジェクトと参照
まず前提として、Python スクリプトにおいては全てのデータはオブジェクトとして扱われます。そしてオブジェクトを使用する際には、オブジェクトを変数で参照して使用することになります。
例えば下記を実行すれば、C言語の場合は変数 x
に整数 15
を格納することになりますが、Python においては変数 x
が整数 15
のオブジェクトを参照することになります。
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_DECREF
の引数に指定して実行することでデクリメントすることができます。Py_DECREF
は関数マクロになっています。
そして、そのデクリメントされた際に参照カウントが 0
になった場合、Py_DECREF
の中でオブジェクトの解放処理が行われるようになっています。
C言語でオブジェクト(PyObject
型のデータ)を扱う際には、free
関数の実行を行わなくても良い代わりに、これらの Py_INCREF
と Py_DECREF
を適切なタイミングで実行する必要があります。
Py_INCREF
や Py_DECREF
については下記ページで解説されています
https://docs.python.org/ja/3.10/c-api/refcounting.html
基本的に、このページでは Py_INCREF
や Py_DECREF
を使って説明をしていきますが、引数で指定するポインタが NULL
である可能性がある場合は、Py_XINCREF
や Py_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_INCREF
や Py_DECREF
を実行するのではなく、適切かつ最小限のタイミングでのみ実行するようになっています。
そして、その適切なタイミングは、次に説明する「参照の所有権」の考え方に則って決められます。
Python/C API もこの考えに則って Py_INCREF
や Py_DECREF
を実行するようになっており、これらを利用する側の関数を実装する際も、「参照の所有権」の考え方に則って Py_INCREF
や Py_DECREF
を実行するようにした方が良いです。
そのため、Python/C API を利用して Python を拡張するような際や、Python からC言語を呼び出す際のラッパー関数を実装する際には、この「参照の所有権」の考え方が非常に重要になります。
スポンサーリンク
参照の所有権
次は、この「参照の所有権」について解説していきます。
所有権と所有権のルール
参照の所有権とは、関数が獲得できる権利と義務になります。この所有権は参照とセットで管理されます。
具体的には、この参照の所有権とは、その参照の所有権を「放棄することができる」という権利になります。
と同時に、関数が終了するまでにその参照の所有権を「放棄しなければならない」という義務でもあります。
参照の所有権を獲得することで、関数には上記の権利と義務が発生します。
所有権の獲得と放棄については追って解説をしていきます
ちなみに、前述で紹介した Py_INCREF
は所有権を獲得するための1つの手段であり、Py_DECREF
は所有権を放棄するための1つの手段となります
要は、参照の所有権を持つ関数は、関数が終了までにその参照の所有権を放棄しなければなりません。逆に、参照の所有権を持たない関数は、その参照の所有権を放棄してはいけません。
これは所有権の考え方に基づいたルールです。以降、このルールを所有権のルールと呼ばせていただきます。
簡単に言えば、所有権とは、所有権を放棄する関数を明確にするためのもの(考え方)です。
C言語でも malloc
関数で確保したメモリを色んな関数で使い回すような場合、どの関数で free
して解放するかをしっかり考えて設計・実装する必要がありますよね?
同様に、所有権は、その解放を行う関数を明確化にするためのものであると考えるとイメージしやすいかと思います。
後述でも解説するように、所有権のルールを守って実装を行えば、Py_INCREF
や Py_DECREF
を毎回実行することなくメモリリークや解放済みのオブジェクトへのアクセスを防ぐことができます。
例えば下の図は、関数がローカル変数 x
でオブジェクト obj1
を、ローカル変数 y
でオブジェクト obj2
を、ローカル変数 z
でオブジェクト obj3
をそれぞれ参照している様子を示しています。
オブジェクトへの参照とは、前述の通りオブジェクトを指す PyObject *
型のポインタです。
この関数はオブジェクト obj1
への参照の「所有権」を所持しています。その他のオブジェクトへの参照の所有権は所持していません。
そのため、この関数はオブジェクト obj1
への参照の所有権のみを放棄することができます。その他のオブジェクトへの参照の所有権は放棄できません。
また、この関数は関数終了までに必ずオブジェクト obj1
への参照の所有権を放棄する必要があります。
ちなみに、上記の x
のような関数が所有権を持つ参照のことを「所有参照」、関数が所有権を持たない参照のことを「借用参照」と呼びます。
ルールを守るのは開発者
ここまでの説明で「参照の所有権を所持していない関数は所有権を放棄できない」と聞いて当たり前の話だと感じた方も多いと思います。だって所有権を所持していないんだから放棄のしようがないですよね…。
ですが、これはプログラムを実装する上では当たり前の話ではありません。なぜなら、所有権のルールを破るような処理の実装もやろうと思えば出来てしまうからです。例えば、所持していない所有権を放棄するような処理を実装することも可能です。
所有権のルールを破ったとしても、つまり所有権を所持していない関数が所有権を放棄したり、所有権を所持している関数が所有権を放棄しなかったりしたとしても、コンパイルでエラーが発生することはありません。
そもそも所有権は変数などのようにソースコードやプログラムの中で目に見える形で存在するものではありません。所有権は開発者の頭の中やドキュメントの中にのみ存在する考え方です。
そのため、コンパイル時に所有権に関するルールが守られているかどうかはチェックすることはできません。
なので、簡単に所有権のルールを破ることができます。
ただし、ルールを破った瞬間、オブジェクトの解放漏れや解放済みのオブジェクトへのアクセスが発生する可能性が生じます。
そのため、安全なプログラムを実現していくためには、Python のオブジェクトを扱うプログラムの開発者は所有権や所有権のルールを理解し、開発中の関数の所有権の有無に基づいてルールをしっかり守りながら実装していく必要があります。
難しそうですが、慣れてくると所有権を意識した実装はそこまで苦にならないと思います。
スポンサーリンク
参照の所有権と参照カウント
また、参照の所有権の考え方を取り入れた場合、オブジェクトの参照カウントとは、そのオブジェクトへの参照の所有権の残数と考えることができます。
そして、Py_INCREF
は引数で指定された参照の所有権を発行する関数マクロと考えることができます(所有権の残数をインクリメントする関数マクロ)。
所有権の発行とは「所有権の獲得」を行う1つの手段となります(他の手段については 参照の所有権の獲得 で解説します)。
それに対し、Py_DECREF
は引数で指定された参照の所有権を破棄する関数マクロ(所有権の残数をデクリメントする関数マクロ)と考えることができます。
所有権の破棄とは「所有権の放棄」を行う1つの手段となります(他の手段については 参照の所有権の放棄 で解説します)。
所有権による放棄を行う関数の明確化
所有権のルールに基づいて実装を行うことで、参照の所有権の放棄を行う関数の明確化を行うことができます。
例えば下の図は、オブジェクトへの参照の所有権を持つ関数 A
から、引数に「オブジェクト obj1
への参照(ポインタ変数 x
)」を指定して関数 B
を呼び出す際の参照や所有権の様子を示す図になります。
基本的には、関数呼び出し時には参照の所有権は移動しません。
なので、obj1
への参照は関数 A
と関数 B
の両方が持っていることになりますが、obj1
への参照の所有権を持っているのは関数 A
のみとなります。そのため、obj1
の参照カウント(所有権の残数)は 1
ということになります。
関数 A
は obj1
への参照の所有権を持っているため、その参照の所有権を放棄することができますが、関数 B
は obj1
への参照の所有権を持っていないため、その参照の所有権を放棄することはできません。
つまり、関数 B
は関数 A
から受け取った参照に対して Py_DECREF
を実行してはいけないことになります(Py_DECREF
は所有権を破棄する関数マクロであり、所有権の破棄は所有権の放棄の1つの手段です)。
また、オブジェクトは Py_DECREF
の中で参照カウントが 0
になった際に解放されます。ですので、関数 B
が関数 A
から受け取った参照に対して Py_DECREF
を実行できないということは、関数 B
が終了して関数 A
に処理が戻ってきた際には、必ず obj1
は存在しているということになります。
なので、関数 A
は関数 B
実行後も安心して obj1
を使用することができます。所有権のルールが守られていれば、関数は、参照の所有権を保持している限り、その参照の参照先のオブジェクトが必ず存在するという前提で処理を行うことができます。
さらに、関数 B
は所有権を持っていないのですから、関数 B
に関しては所有権を放棄することを考慮せずに実装することができます(少なくとも関数 A
から受け取った参照に関しては)。
つまり、所有権のルールに従えば、関数は所持している参照の所有権の放棄さえ行えば良いということになります(Py_DECREF
の実行など)。
なので、Py_INCREF と Py_DECREF の実行タイミング で説明したような、参照が不要になった際 or 参照がなくなる際に毎回 Py_DECREF
を実行するという面倒なことはしなくても良いのです。
このように、所有権を放棄できる関数を明確化しておくことで、意図しないタイミングでのオブジェクトの解放を防いだり、プログラマーの実装の負担を軽減したりすることが可能になります。
ルールを破ることの危険性
ただし、所有権のルールを破ってしまうと、オブジェクトの解放漏れや解放済みのメモリへのアクセス等が発生する可能性があるので注意してください。
例えば先程の例で考えれば、関数 B
が obj1
への参照の所有権を持っていないにも関わらず Py_DECREF
を実行して所有権を放棄してしまうと、obj1
の参照カウントが 0
になって obj1
が解放されてしまいます(実行した関数が参照の所有権を保持しているかどうかに関わらず、Py_DECREF
はできてしまうのです…)。
ですが、関数 A
は自身が obj1
への参照の所有権を持っているのですから、関数 B
で obj1
の解放が行われているとは夢にも思いません。なので、当然関数 B
実行後にも obj1
を利用する可能性があります。
そうなると関数 A
は解放済みのオブジェクトにアクセスすることになり、メモリアクセス違反が発生することになります。
逆に、関数 B
がルールを守って所有権の放棄を行わなかった場合でも、もし関数 A
が義務を怠ってオブジェクトの参照の所有権を放棄せずに関数を終了してしまうと、所有権を持つ関数が存在しないにもかかわらずオブジェクトが残ってしまうことになります。
前述の通り、参照の所有権を持つ関数のみが所有権の放棄を行えるわけですので、所有権を持つ関数が存在しなくなると、その参照に対して Py_DECREF
が実行可能な関数が存在しなくなることになります。
つまり、このオブジェクトはプログラムが終了するまで残り続けることになり、オブジェクトの解放漏れ(メモリリーク)が発生することになります。
このように、所有権のルールを破ってしまうと、一気にプログラムの動作が不安定になります。
なので、オブジェクトの参照を行う関数を実装する際には、その関数がどの参照の所有権を持ち、どの参照の所有権を持っていないかをしっかり意識しながら実装していく必要があります。
所有権の移動
また、所有権は関数間で移動することもあるので注意してください。
所有権の移動の一例として、関数からの参照の返却が挙げられます。関数から呼び出し元に対してオブジェクトへの参照を返却する場合、所有権も含めて返却を行うことができます。
例えば、関数 A
から関数 B
を呼び出し、
次に関数 B
内部で obj1
を生成し、さらに obj1
への参照の所有権を発行して獲得したとします(Py_INCREF
の実行)。
さらに関数 B
が、その obj1
への参照を return
で返却した場合、関数 B
は終了し、呼び出し元の関数 A
は参照と一緒にその参照の所有権も受け取ることになります。つまり関数 B
から関数 A
に obj1
への参照の所有権が移動し、関数 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 はその参照の所有権を呼び出し側に委譲します。
それに対し、PyList_GetItem
というリストオブジェクトからオブジェクトを取得する API においては、API の返却値の型は PyObject *
でオブジェクトへの参照を返却する API ではあるものの、参照の所有権の委譲は行いません。
前述の通り、参照の所有権を持っていない関数は、その参照の所有権を放棄することができません。また、参照の所有権を放棄する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 からオブジェクトへの参照が返却されるとともに、その参照の所有権が委譲されることになります。
引用元:シーケンス型プロトコル
それに対し、返却値が Borrowed reference
である場合、API からオブジェクトへの参照が返却されますが、その参照の所有権は委譲されません。
引用元:リストオブジェクト
こんな感じで、マニュアルから所有権が委譲されるかどうかを確認することができますので、オブジェクトへの参照が返却される 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;
}
一旦 y
で g
の参照を受け取る形にしていますが、直接 Py_XDECREF(g)
でグローバル変数に保存されている所有権を放棄しても良いです。
下記のように記述するとメモリリークが発生するので注意してください。
PyObject *g = NULL;
void
func1(PyObject *z) {
/* 所有権を発行 */
Py_INCREF(z);
/* 所有権を保存 */
g = z;
/* gに格納されていた参照が取得できない */
}
このように、グローバル変数に保存した所有権はそのグローバル変数を利用した関数に即座に移動することになると考え、さらに所有権のルールを守るためにはどういう順番で処理を行えば良いかを意識しながら実装するようにすれば、安全なプログラム(メモリリークが発生しないプログラム)を実現しやすくなると思います。
いずれにせよ、保存による所有権の放棄は結構ややこしいと思いますので、不要であれば保存はしない方が良いと思いますし、保存せざるを得ない場合は特に所有権を意識しながら実装するのが良いと思います。
所有権の盗み取り
関数が所有権を放棄する手段は、ここまで紹介してきた破棄・委譲・保存の3パターンになります。
受動的な所有権の放棄
が、所有権は他の関数から盗み取られる可能性があり、これによって所有権を失うことがあるので注意してください。
破棄や委譲・保存に関しては、所有権を持つ関数自らが能動的に所有権の放棄を行いますが、盗み取りの場合は他の関数によって受動的に所有権の放棄を行うことになります。
下記の Python/C API リファレンスマニュアルにおいては、所有権の盗み取りではなく “参照の盗み取り” という単語が使われているので注意してください
用語としては当然公式のリファレンスマニュアルの方が正しいです
が、ここまでの解説の流れからすると “所有権の盗み取り” と言った方が分かりやすいかと考え、所有権の盗み取りという用語を利用しています
いずれにしても、関数実行によって所有権を失うという点では同じです
といっても、所有権の盗み取りが発生するのは特定の関数や API を呼び出した時のみであり、盗み取りされる所有権は「呼び出し時に引数に指定した参照」の所有権のみとなります。
所有権を盗み取る Python/C API の1つが PyList_SetItem
になります。
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 仕様に明記されています(参照を盗み取ると記述されている)。
引用元:リストオブジェクト
所有権の盗み取りを行う 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_INCREF
と Py_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_INCREF
と Py_DECREF
を実行すべきタイミングを考えてみましょう!
ここから紹介する関数は全て、Python スクリプトから呼び出し可能な関数をC言語で実装した例となります。
Python スクリプトから呼び出すための手順は下記ページで解説していますので、実際に動作させてみたい場合は下記ページを参考にしてください(これから紹介する関数は、下記ページにおけるラッパー関数の位置付けになります)。
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_None
は None
オブジェクトへの参照となります。
この関数のどこに問題があるでしょうか?
#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
して所有権を委譲する必要が有ります。
#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 から所有権が委譲されるかどうかをしっかり確認しながら考えてみてください。
#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
関数の誤りであり、次のように修正する必要があります。
#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 が違います。その違いに注目して問題点について考えてみてください。
#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
実行後 〜 関数終了の間に実行する必要があります。
#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つのオブジェクトを受け取り、それらを格納したリストを返却する関数となります(所有権も委譲)。
この関数のどこに問題があるでしょうか?
#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
で獲得しておく必要があります。
#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 の時とは実現の仕方が異なっており、下記の関数の実装には問題があります。どこに問題があるでしょうか?
#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
の実行が不要なため、そこをそのまま削除してやれば良いだけです。
#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
関数を実行するとメモリリークが発生します。
メモリリークを防ぐためにどう修正すれば良いでしょうか?
#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
関数のみになりますが、_new
も g_ref
を利用しているのですから、_new
関数でも同様の所有権の放棄を行う処理を追加した方が無難です。
ちなみに、g_ref
は初期値を NULL
としていますので、g_ref
に格納された参照の所有権を破棄する際には Py_DECREF
ではなく Py_XDECREF
を利用する方が良いです。
また、func
関数は _new
関数や _set
関数から返却される参照を受け取っていませんが、これらの関数からは所有権は委譲されない、つまりこれらの関数の返却値は Borrowed reference
なので、これによってメモリリークが発生することはありません。
#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_INCREF
や Py_DECREF
の実行箇所を最小限に抑え、かつ、メモリリークやメモリアクセス違反・メモリの二重解放を防ぐことを実現することができます。
基本は、参照の所有権を持つ関数は「その参照の所有権を放棄しなければなない」&参照の所有権を持たない関数は「その参照の所有権を放棄してはいけない」というルールを忠実に守れば良いだけです。
ただし、このルールを守るためには、関数が参照の所有権を所持しているかどうかを実装者が把握できていなければいけません。この所有権を所持しているかどうかを把握するためには、利用する関数や API の仕様を理解しておくことが重要になります。
なので、オブジェクトの参照を扱う関数を実装する際は、使用する関数や API の仕様をより一層しっかり理解することを心がけましょう!