仕事でPythonの関数をC++で書き換えることがあったので、調べたことをまとめておく。

基本的には公式を読めば良いのだが、簡単な関数を作るだけなら不要な詳細も多いので、 最低限必要なものだけ抜き出して記録しておく。 それでも、けっこう多いな・・と思うんだけど、ほとんどは作法に則ってコピペしましょう、という話でしかない。

用意するもの

ライブラリ名

何でも良いが、ここではhogeとしておく。

ソースファイル

ファイル名は、<modulename>module.cppとするのが慣習らしい。 もしくは<modulename>.cppでも良い。 今の場合は、hogemodule.cpp or hoge.cppである。

ヘッダの読み込み

ファイルの先頭2行は

#define PY_SSIZE_T_CLEAN
#include <Python.h>

とする。

(emacs用)linter用の設定

<Python.h>はinclude pathを設定しないと見つからない。ファイルの最後に

// Local Variables:
// flycheck-gcc-include-path: ("/path/to/Python.h")
// flycheck-clang-include-path: ("/path/to/Python.h")
// End:

を書いておけばflycheckが見つけてくれる。/path/to/Python.hは、locate Python.hをターミナルで実行する。

関数の実装

サンプルとして、2つのdoubleを受け取り1-ノルム, 2-ノルムを返す関数を実装する。

struct norms {
  double one;
  double two;
};

norms func(const double& x, const double& y){
  return {
    abs(x) + abs(y),
    sqrt(x*x + y*y)
  };
}

Pythonとのインターフェイス

Pythonのインタプリタの中では、全てのもの(数値やbooleanでも)はPyObjectというオブジェクトである。 このPyObject型の値を通常のdoubleやbooleanに変換する作業が必要である。

static PyObject* hoge_func(PyObject *self, PyObject *args){
  double x, y;

  if (!PyArg_ParseTuple(args, "dd", &x, &y))
    return NULL;

  norms vals = func(x, y);

  PyObject* ret = PyList_New(0);
  PyObject* p;
  p = PyFloat_FromDouble(vals.one);
  PyList_Append(ret, p);
  p = PyFloat_FromDouble(vals.two);
  PyList_Append(ret, p);

  return ret;
}

位置引数だけを取る関数を定義する場合は、上のように2つのPyObject*引数を受け取れるようにしておく。 上の例でselfは、ここでは使わないので無視(実際には、モジュールあるいはオブジェクトへのポインタが渡ってくるらしい)。 2つ目のargsに、実際の引数が渡されてくる。

PyObject*を通常の型に翻訳するにはPyArg_ParseTupleを使う。PyArg_ParseTupleは2つの引数に加えて、 任意個の引数を取る。1つ目は引数に渡ってきたPyObject*を渡す。

2つ目は、期待している引数の型を書式文字列として渡す。書式は、intなら”i”、doubleなら”d”である。その他が 使いたい場合は公式を見る。今の場合はdoubleが2つなので”dd”を渡している。

3つ目以降に、翻訳してくれた値を格納する場所を指すポインタを渡す。

何らかの問題があった場合、!PyArg_ParseTuple(...)falseに評価されNULLが返却される。 これは、Pythonの例外ハンドリングのお作法に則った仕草。

最後に返り値を準備する。Pythonの世界では全てがPyObjectなので、返り値はPyObject*に変換しなければならない。 intfloatなど基本的な型への変換には、PyXxx_FromYyy関数を使う(XxxにはPythonの型名、Yyyにはc++での型名が入る)。

さらに今回は、2つの値を返したいので、これらをリストに詰めて返却することにする。 リストはPyList_Newで作成し、PyList_Appendで要素を追加する。

パッケージ化

パッケージ情報を、お作法に則ってまとめる。これは、サンプルをコピペして必要な部分を編集すれば良い。

static PyMethodDef HogeMethods[] = {
  {
    "func",    // Pythonから見える名前
    hoge_func, // 関数の実体
    METH_VARARGS,
    "calculate norms" // 説明
  },
  {NULL, NULL, 0, NULL} // 番兵
};

static struct PyModuleDef hogemodule = {
  PyModuleDef_HEAD_INIT,
  "hoge",          // モジュール名
  "sample module", // ドキュメント
  -1,              // -1で固定(詳細な意味は調べてない)
  HogeMethods      // 上で定義したPyMethodDefの配列
}

PyMODINIT_FUNC
PyInit_hoge(void){
  return PyModule_Create(&hogemodule);
}

PyMethodDefの3つ目の要素は、引数のタイプを表すフラグである。 引数のタイプは、今回のように位置引数だけの場合はMETH_VARARGSを指定する。 キーワード引数を指定したい場合はドキュメントを参照する。

PyMODINIT_FUNCをつけたPyInit_<パッケージ名>という関数を最後に用意する。 Pythonはimport <パッケージ名>すると、この規約に則った関数名を共有ライブラリの中から探してくるっぽい。 なので、この関数の名前は規約どおりにPyInit_<パッケージ名>としなければならない。 また、この関数の前にはPyMODINIT_FUNCマクロを置く必要がある。 まぁ詳細は気にせずコピペする・・

setup.py

これも目をつぶってコピペする・・

from distutils.core import setup, Extension

module1 = Extension('hoge',
                    sources=['hoge.cpp'])

setup(name='hoge',
      version='1.0',
      description='This is a demo package',
      ext_modules=[module1])

ビルド

hoge.cppsetup.pyを同じフォルダに置いて

$ python setup.py build

を実行する。これで、build/以下に.soファイルができる。.soファイルのある フォルダをsys.pathに追加すれば、import hogeが使えるようになる。

デバッグ

c++から普通に標準出力に出力すれば、Pythonプロセスの標準出力に出てくるのでprintデバッグはできる。

または、#ifdefを使って、単体実行用のソースとパイソンのパッケージ化の部分を分けると、実行バイナリを 作る事もできる。

例外

Python側に例外を投げる一番簡単な方法は、

PyErr_SetString(PyExc_RuntimeError, "panic");
return NULL;

である。関数の入り口で入力値の検証を行うことは、 意図しない動作をしている時にPython側の間違いかC++側の間違いかを見極めるのに非常に重要なので、 拡張を書く場合には問答無用で、とりあえず入り口で検証を行った方が良い。

ソースファイル全体

#define PY_SSIZE_T_CLEAN
#include <Python.h>

#include <cmath>
#include <iostream>

using namespace std;

struct norms {
  double one;
  double two;
};

norms func(const double& x, const double& y){
  return {
    abs(x) + abs(y),
    sqrt(x*x + y*y)
  };
}

#ifdef HOTOKU_DEBUG

int main(){
  auto ret = func(1, 2);
  cout << ret.one << "," << ret.two << endl;
}

#else

static PyObject* hoge_func(PyObject *self, PyObject *args){
  double x, y;

  if (!PyArg_ParseTuple(args, "dd", &x, &y))
    return NULL;
  norms vals = func(x, y);
  PyObject* ret = PyList_New(0);
  PyObject* p;
  p = PyFloat_FromDouble(vals.one);
  PyList_Append(ret, p);
  p = PyFloat_FromDouble(vals.two);
  PyList_Append(ret, p);
  return ret;
}

// 関数情報のリスト
static PyMethodDef HogeMethods[] = {
  {
    "func",           // Pythonから見える関数名
    hoge_func,        // 関数の実体
    METH_VARARGS,     // 引数のタイプ
    "calculate norms" // ドキュメント
  },
  {NULL, NULL, 0, NULL} /* Sentinel */
};

// モジュールの情報
static struct PyModuleDef hogemodule = {
  PyModuleDef_HEAD_INIT,
  "hoge",          // モジュール名
  "sample module", // ドキュメント
  -1,              // -1固定(詳細を調べてない)
  HogeMethods      // 上で定義した関数のリスト
};


/* モジュールの定義
   1. PyMODINIT_FUNCを付ける
   2. PyInit_<モジュール名> という命名規約に従う
*/
PyMODINIT_FUNC
PyInit_hoge(void){
  return PyModule_Create(&hogemodule); // 上で定義したモジュール情報を渡す
}

#endif

// Local Variables:
// flycheck-gcc-include-path: ("/usr/local/anaconda3/include/python3.8")
// flycheck-clang-include-path: ("/usr/local/anaconda3/include/python3.8")
// End: