雑食性雑感雑記

知識の整理場。ため込んだ知識をブログ記事として再構築します。

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」のエラーで読み込み失敗。
    • クラス内デストラクタを定義したはいいものの実装していなかったのが原因。定義と実装揃ってないとエラー出る。(わかりやすいエラー出してくれ。。。)

その他

参照

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++化して高速化できそう。

参照

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 とのつなぎ方も分かったし、これで複雑なコードも気楽にビルドできるでしょう。
( そこまで複雑な処理を作れるかどうかはともかく。。 )

参照

Python で使うと便利なライブラリ (2016/08/18 更新)

Python で便利だと思って使うようになったライブラリのメモ。
知識増えて使えるようになったら随時更新 (最終更新 2016/08/18)

標準ライブラリ

all, any

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

  • 例えば数え上げ処理
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

# -*- 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」を使うと、設定ファイルをパースして使えて便利!!
  • …なのだが、値は全て文字列なので、そこから適切な形に変換しないと――。
  • 別途設定ファイルのための設定を作り、制御できるようにしてみた。
続きを読む