Cython で Python ⇔ C++ のインタフェースを作る
以前、Python と C++ を繋ぐためには SWIG を使ってたが、
Cython 0.13 以降では Cython 自身で Python と C++ を繋ぐことができる。 (2018/05時点で 0.28)
この機能を試してみた。
ここまで来てようやく cythonize を知ったので、ビルドはそちらで。
環境前提
- Ubuntu 16.04
- Python 3.5
- Cython 0.28
- その他必要なものはインストール済みとする。
サンプル概要
2つのクラスを作成し、連携して動作させてみる簡単なサンプル。
1. file_reader ... 1行1整数のデータを読み込んでC++メモリ上で保持。
2. calculator ... file_reader をメンバとして持ち、中のデータを使って計算。
ファイル構成
- setup.py ... ビルド設定ファイル - file_read.pyx ... cython ソースコードファイル - FileReader.cpp ... C++ クラス実装 - FileReader.h ... C++ クラス定義 - test.txt ... サンプルデータファイル
実装
setup.py
cythonize を使ってビルドするための設定を定義
0.17 以降は
- 基本的な設定 → setup.py
- C++コードとのリンク情報 → .pyx
に区分けされている。
from distutils.core import setup from Cython.Build import cythonize setup( name = "file_read_app", ext_modules = cythonize("*.pyx"), extra_compile_args=["-std=c++11"], extra_link_args=["-std=c++11"], )
(2018/05/31追記)
実は上記C++11設定は効いていなかったことが判明。。
以下 export したのが効いてた様子。ビルド前に export で設定追加しておく。
上記設定は古いっぽい?
$ export CFLAGS='-std=c++11'
Tips
- extra_compile_args や extra_link_args でオプションを追加。C++11設定を追加しておく。
C++ コード
C++ 側で実際の処理を作る。
FileReader.h
#include <fstream> #include <iostream> #include <string> #include <vector> namespace reader { class FileReader { public : FileReader(std::string filePath); ~FileReader(); public : int getSize(); int getData(int index); void add(int datum); public : std::string filePath; private : std::vector<int> data; }; class DataCalc { public : DataCalc(FileReader *fileReader); ~DataCalc(); public : int sum(); private : FileReader *fileReader; }; }
FileReader.cpp
#include "FileReader.h" using namespace reader; FileReader::FileReader(std::string filePath) { // Read file and save data std::cout << "Read file : " << filePath << std::endl; this->data = std::vector<int>(); std::ifstream ifs(filePath.c_str()); if (ifs.fail()) { std::cout << "Error : Failed to open file." << std::endl; return; } std::string str; while (getline(ifs, str)) { data.push_back(std::stoi(str)); } } FileReader::~FileReader() {} int FileReader::getSize() { return this->data.size(); } int FileReader::getData(int index) { if (index >= this->data.size()) { std::cout << "Error: Index number too large (" << index << " < " << this->data.size() << ")." << std::endl; return -1; } return this->data[index]; } void FileReader::add(int datum) { this->data.push_back(datum); } DataCalc::DataCalc(FileReader *fileReader) { this->fileReader = fileReader; } DataCalc::~DataCalc() {} int DataCalc::sum() { int ret = 0; int size = this->fileReader->getSize(); for (int i = 0; i < size; i++) { ret += this->fileReader->getData(i); } return ret; }
Cython コード
Cython 上では C++ のクラスを呼ぶため、こちらでもクラス定義する必要ある (これだけちょっと面倒。。。)
Python ⇔ C++ のインタフェース定義もここで。
file_read.pyx
# distutils: language = c++ # distutils: sources = FileReader.cpp from libcpp.string cimport string cdef extern from "FileReader.h" namespace "reader" : cdef cppclass FileReader : FileReader(string) except + int getSize() int getData(int) void add(int) cdef cppclass DataCalc : DataCalc(FileReader*) except + int sum() cdef class PyFileReader : cdef FileReader *thisptr def __cinit__(self, bytes path) : cdef string s_path = path self.thisptr = new FileReader(s_path) def __dealloc__(self) : del self.thisptr def get_size(self) : return self.thisptr.getSize() def get_data(self, int index) : return self.thisptr.getData(index) def add(self, int datum) : self.thisptr.add(datum) cdef class PyCalc : cdef DataCalc *thisptr def __cinit__(self, PyFileReader file_reader) : self.thisptr = new DataCalc(file_reader.thisptr) def __dealloc__(self) : del self.thisptr def sum(self) : return self.thisptr.sum()
Tips :
- 「except +」はおまじない的に付けとけばよい (本家ページに理由書いてある)
- 文字列の扱い
- Python から入ってくるものは (型推論に期待しても良いがせっかくなので) bytes 型で受け取る。
- C++ には string で渡したいので、変換かける。
ビルド
ビルドしてエラー起きなければ OK 。
「-i」付けることで、ビルド実行したディレクトリ内に .so ファイルが生成される。
python3 setup.py build_ext
実行
test.txt
30 40 50 110
ipython で実行
$ ipython3 Python 3.5.2 (default, Nov 23 2017, 16:37:01) Type 'copyright', 'credits' or 'license' for more information IPython 6.3.1 -- An enhanced Interactive Python. Type '?' for help. In [1]: import file_read In [2]: reader = file_read.PyFileReader(b"test.txt") Read file : test.txt In [3]: reader.get_size() Out[3]: 4 In [4]: reader.get_data(2) Out[4]: 50 In [5]: c = file_read.PyCalc(reader) In [6]: c.sum() Out[6]: 230
できた!!
トラブルシューティング的な
- XXXX.pyx と XXXX.cpp (C++ソースコード) の名前を同じにしてはいけない。
- pyx コンパイル時に XXXX.cpp が作り出され、重複定義と言われてしまう。
- コンパイルに成功したが、python で import 時に「undefined symbol」のエラーで読み込み失敗。
- クラス内デストラクタを定義したはいいものの実装していなかったのが原因。定義と実装揃ってないとエラー出る。(わかりやすいエラー出してくれ。。。)
その他
参照
- 英語だけど最新の情報を見た方が参考になる。(有志による和訳版あるが0.17で止まってる)
Boost.numpy ことはじめ その2 (戻り値)
前回まででPythonからサンプルを使ってみることができるようになった。
今回は、2次元numpy配列をC++側で作成し、Python側に戻せるようにしてみる。
サンプル
「行列サイズを指定し、そのサイズにあった2次元numpy行列を返す」サンプルを作ってみる。
ディレクトリ構成
<root directory> - sample.cpp - CMakeLists.txt
sample.cpp
np::zeros を使ってゼロ配列を作成し、そこに値を入れ込む形で実装する。(場合によってはもっと高速な方法あるかもだけどとりあえず。)
2次元ができてしまえば、1次元はもっと簡単なので省略。
ポイント :
- np::zeros の引数は「shape のサイズ、shape、タイプ」。基本的にはPythonのものと同じ。
- 1次元vectorでデータを用意し、std::copyでデータを移す。
#include "boost/python/numpy.hpp" #include <stdexcept> #include <algorithm> namespace p = boost::python; namespace np = boost::python::numpy; std::vector<double> func(int row, int col) { std::vector<double> v; int count = 0; for (int y = 0; y < row; y++) { for (int x = 0; x < col; x++) { v.push_back(count++); } } return v; } np::ndarray retvec(int row, int col) { std::vector<double> v = func(row, col); Py_intptr_t shape[2] = {row, col}; np::ndarray result = np::zeros(2, shape, np::dtype::get_builtin<double>()); std::copy(v.begin(), v.end(), reinterpret_cast<double*>(result.get_data())); return result; } /* BOOST_PYTHON_MODULE の引数は .so 名 */ BOOST_PYTHON_MODULE(libsample) { Py_Initialize(); np::initialize(); p::def("retvec", retvec); }
ビルド
CMakeLists.txtは前回と同じなので割愛。
mkdir build cd build cmake .. make
実行
ipython3で実行
ipython3 In [1]: import libsample as s In [2]: import numpy as np In [3]: a = s.retvec(3, 2) In [4]: a Out[4]: array([[0., 1.], [2., 3.], [4., 5.]])
3行2列の配列ができた!!
まとめ
前回と合わせて、
・ Boost.Numpy の実行
・ (C++で処理して) numpy 配列を戻す
ところまでできた。
ここまでで大体の処理はC++化して高速化できそう。
参照
- 1. how to return numpy.array from boost::python?
- stackoverflow。Boost.Numpyで戻り値取ってくるためのQ&A。
Boost.numpy ことはじめ
以前から「Boost.numpy」というのが便利というのを聞いていたがなかなか使って見る機会が無く。。
他のブログ等検索したが、自分にちょうど良さそうな記事が見つからなかったので整理しつつ事始めしてみる。
Ubuntu16.04で環境構築から始めて、簡単なサンプルをcmakeビルドし、Pythonで呼び出すところまで。
Boost.numpy
Boost 1.63 辺りから追加された機能。
C++ boost code ⇔ Python 間で numpy 配列を受け渡し、
Python だと遅いところは C++ で操作
C++ で記述が面倒なところは Python で操作
することができる。
環境
- Ubuntu16.04
- Python3.5
- ( boost は 1.58 が apt-get により既に導入済み )
- その他
- CMake 3.5 ( ソースコードからビルドで導入 )
環境構築
最新版 Boost
/opt 上に boost 環境を構築した。書いた時点で最新は1.67。
デフォルトだとPython2.7の方に行ってしまうので、bootstrap にてPython3を使うようにしてあげる。
環境汚さないように、ビルドしたものは /opt 内に。
sudo su - cd /opt/ wget https://dl.bintray.com/boostorg/release/1.67.0/source/boost_1_67_0.tar.gz tar zxvf boost_1_67_0.tar.gz cd boost_1_67_0 ./bootstrap.sh --with-python-version=3.5 ./b2 --prefix=/opt/boost_1_67_0 install
確認
find / -name *boost_numpy* ( 中略 ) /opt/boost_1_67_0/lib/libboost_numpy35.so
使ってみる
サンプルソースコード
参照1のコードを使わせてもらう。インクルード等ちょっと修正。
ポイント :
- BOOST_PYTHON_MODULE の第一引数にはPythonで実行時のモジュール名。
- 後で cmake ビルドで「lib~」となるので見越した名前を付ける。
sample.cpp
#include "boost/python/numpy.hpp" #include <stdexcept> #include <algorithm> namespace p = boost::python; namespace np = boost::python::numpy; /* 2倍にする */ void mult_two(np::ndarray a) { int nd = a.get_nd(); if (nd != 1) throw std::runtime_error("a must be 1-dimensional"); size_t N = a.shape(0); if (a.get_dtype() != np::dtype::get_builtin<double>()) throw std::runtime_error("a must be float64 array"); double *p = reinterpret_cast<double *>(a.get_data()); std::transform(p, p + N, p, [](double x) { return 2 * x; }); } /* BOOST_PYTHON_MODULE の引数は .so 名 */ BOOST_PYTHON_MODULE(libsample) { Py_Initialize(); np::initialize(); p::def("mult_two", mult_two); }
ビルド設定
CMake でビルド。
ポイント :
- boost は独自インストールなので、それに対応した読み込み。
CMakeLists.txt
project(sample) cmake_minimum_required(VERSION 3.0) set(BOOST_ROOT /opt/boost_1_67_0) ### C++11 add_compile_options(-std=c++11) ### pkgconfig (for pkg_check_modules) find_package(PkgConfig REQUIRED) ### Python includes pkg_check_modules(PYTHON3 python3 REQUIRED) include_directories(${PYTHON3_INCLUDE_DIRS}) ### Boost includes include_directories(${BOOST_ROOT}/include) link_directories(${BOOST_ROOT}/lib) ### Build add_library(sample SHARED sample.cpp) set_target_properties(sample PROPERTIES SUFFIX ".so") target_link_libraries(sample boost_numpy35 boost_python35)
ちなみに、ここまでのディレクトリ構成は
<root directory> - sample.cpp - CMakeLists.txt
となっている想定。
root directoryのところからビルドする。
mkdir build cd build cmake .. make
正常にビルドされると libsample.so ができる。
実行
読み込んでみる。 (ipython3)
ipython3 In [1]: import libsample as s In [2]: import numpy as np In [3]: a = np.array([1.0, 2.0], np.float64) In [4]: s.mult_two(a) In [5]: a Out[5]: array([2., 4.])
まとめ
Boost.Numpy の実行試せた。
CMake とのつなぎ方も分かったし、これで複雑なコードも気楽にビルドできるでしょう。
( そこまで複雑な処理を作れるかどうかはともかく。。 )
参照
- 1. https://qiita.com/termoshtt/items/81eeb0467d9087958f7f
- C++でPythonを拡張するためのBoost.NumPyチュートリアル(実践編)
C++ スレッド処理で複数スレッド連携
C++ の Thread と Queue を使って、複数のスレッドを連携させて動作させるサンプルを作ってみた。
続きを読むPython で使うと便利なライブラリ (2016/08/18 更新)
Python で便利だと思って使うようになったライブラリのメモ。
知識増えて使えるようになったら随時更新 (最終更新 2016/08/18)
標準ライブラリ
all, any
- http://docs.python.jp/3.5/library/functions.html#all:Python3.5 all
- 引数の iterable のすべての要素 (or どれか1つの要素) が真なら True を返す処理。
t = [True, True, True] f = [True, True, False] print(all(t)) # True print(all(f)) # False print(any(f)) # False
- 2つのリストのチェックに使ってみた
la = [1, 2, 3] lb = [1, 2, 4] ### All 関数なし flag = False if len(la) == len(lb) : flag = True for a, b in zip(la, lb) : if a != b : flag = False break print(flag) ### All 関数あり。 print(len(la) == len(lb) and all([a == b for a, b in zip(la, lb)]))
collections.defaultdict
- http://docs.python.jp/3.5/library/collections.html#collections.defaultdict:Python3.5 defaultdict
- collectionsは通常のlist, dict等のサブクラス群で、便利な処理が揃っている。
- 引数 default_factory に「対応するkeyが無かったときに生成されるvalueの型」を入れる。
- int を入れると、新規keyのvalueとして0が入って自動生成。
- 引数 default_factory に「対応するkeyが無かったときに生成されるvalueの型」を入れる。
- 例えば数え上げ処理
import random data = [random.randint(0, 10) for i in range(100)] ### defaultdict 無し counter01 = {} for datum in data : if datum not in counter01 : counter01[datum] = 1 else : counter01[datum] += 1 print(counter01) ### defaultdict あり from collections import defaultdict counter02 = defaultdict(int) for datum in data : counter02[datum] += 1 print(counter02)
3rd Party
docopt
- https://github.com/docopt/docopt:git docopt
- Description や Usage を書くのと起動引数パーサ書くのが同時にできて便利!!
- 日本語 Description も OK。
# -*- coding: utf-8 -*- from docopt import docopt __doc__ = """ Description: Docopt test module 日本語説明も大丈夫 Usage: {f} [-h | --help] {f} [-v | --version] {f} [<opt>] --arg0=<arg> Options: -h --help Show this screen. --version Show version. <opt> Option0 --arg0=<arg> Argument0 """.format(f = __file__) if __name__ == '__main__' : args = docopt(__doc__, version = "0.0.1") print("arg0 = {0}".format(args["--arg0"])) print("opt0 = {0}".format(args["<opt>"]))
- 実行結果
$ python test.py yyy --arg0=xxx arg0 = xxx opt0 = yyy $ # 「[]」で囲った <opt> 分は省略可能 $ python test.py --arg0=xxx arg0 = xxx opt0 = None $ # --arg0 は指定必須。フォーマットが違うと Usage 表示 $ python docopt_test.py yyy Usage: docopt_test.py [-h | --help] docopt_test.py [-v | --version] docopt_test.py [<opt>] --arg0=<arg>
Python で Multi process (して、更に Signal で安全に終了させる)
概要
- Python のマルチプロセス実装を試してみた。
- 更に、signal を取り入れて「Ctrl + C」や「kill」で安全に全プロセスを終了させるようにしてみた。
PythonのConfigParserでカッチリとしたコンフィグ設定をする
概要
- Pythonのモジュール「ConfigParser」を使うと、設定ファイルをパースして使えて便利!!
- …なのだが、値は全て文字列なので、そこから適切な形に変換しないと――。
- 別途設定ファイルのための設定を作り、制御できるようにしてみた。