Cythonの問題点
Cythonの活用法
Cython
で実装を書くのではなく、C/C++
で実装したものをPython
から呼び出すためのインターフェイスを提供するツールとして用いるのがベストだと思う。
Cython
を書く量を最小限にして、ヒューマンエラーを抑えていこうという作戦。
具体的な手順
目標:高速なモジュールの作成
アウトライン
- (Cythonパッケージやビルドツールのインストール)
C/C++
で実装Python
から呼びたいものだけCython
でラップ- 使いやすいように
Python
でラップ
例はC++
で行うがC
でも同様。
ヘッダの作成
実装するものをまとめたヘッダファイルを作成
// my_module_c.h #ifndef MY_MODULE_C_H #define MY_MODULE_C_H #include <vector> #include <string> // Goに倣って、外部に公開する関数はキャピタライズ double Sum_c(std::vector<double>& data); class MyClass_c { public: MyClass_c(std::string name); void PrintCount(); private: // Pythonに倣って_を付けることで明示的にprivateと宣言 // 引数との名前衝突も避けられる std::string _name; int _count; void addCount(); }; #endif // MY_MODULE_C_H
実装
C++
で処理を実装する
// my_module_c.cpp #include "my_module_c.h" #include <iostream> double Sum_c(std::vector<double>& data) { double result = 0; for (double v : data) { result += v; } return result; } MyClass_c::MyClass_c(std::string name) { _name = name; _count = 0; } void MyClass_c::PrintCount() { std::cout << "I'm " << _name << ".\n"; std::cout << "My count is " << _count << ".\n"; addCount(); } void MyClass_c::addCount() { _count++; }
コラム::テストしてみる
Cython
に行く前に、C/C++
で書いたコードが確かに動くか確認してみる。
// my_module_c_test.cpp #include "my_module_c.h" #include <iostream> using namespace std; int main() { const int n = 3; vector<double> data = {2.4, 4.6, 5.0}; double total = Sum_c(data); double expected = 12.0; cout << "Sum_c test " << "result: " << total << ", expected: " << expected << ", " << ((total == expected)? "ok" : "err") << endl; cout << "MyClass test" << endl; MyClass* ptr = new MyClass("Bob"); if (ptr == nullptr) { cout << "instance not created" << endl; exit(1); } cout << "MyClass::PrintCount" << endl; for (int i = 0; i < 3; i++) { ptr->PrintCount(); } delete ptr; }
コンパイル&実行
g++ -c my_module_c.cpp -o my_module_c.o g++ my_module_c_test.cpp my_module_c.o -o my_module_c_test Remove-Item my_module_c.o ./my_module_c_test Remove-Item my_module_c_test.exe
実行結果
$ ./test_c Sum_c test result: 12, expected: 12, ok MyClass test MyClass::PrintCount I'm Bob. My count is 0. I'm Bob. My count is 1. I'm Bob. My count is 2.
Cython
で実装するとデバッグが困難だが、C/C++
単体で動くように実装することでC/C++
のサポート溢れるデバッグツールでデバッグが可能。
お片付け
ファイルが増えてきたのでお片付け
$ mkdir src $ mv my_module_c.cpp ./src/my_module_c.cpp $ mkdir include $ mv my_module_c.h ./include/my_module_c.h
Cythonでラップ
Python
に公開したい関数だけCython
でラップ。
これがインターフェースとなる。
# my_module_cy.pyx from libcpp.vector cimport vector from libcpp.string cimport string cdef extern from "my_module_c.h": # extern fromでヘッダ読み込み # namespace有り: cdef extern from "some.h" namespace "somename": double Sum_c(vector[double]& data) cdef cppclass MyClass_c: MyClass_c(const string name) void PrintCount() def sum_cy(vector[double] data: list[float]) -> float: return Sum_c(data) cdef class MyClass_cy: # インスタンス変数の列挙 cdef MyClass_c* _ptr def __cinit__(self, *args, **kwargs) -> None: # cレベルの初期化 # __init__関数と同じ引数で呼ばれる pass def __init__(self, name: str) -> None: cdef string _name = bytes(name, encoding='utf-8') self._ptr = new MyClass_c(_name) def __dealloc__(self) -> None: # cレベルのメモリ開放 del self._ptr def print_count(self) -> None: self._ptr.PrintCount()
コンパイル
Cython
コード→C/C++
コード- コンパイル & ビルド
という2段階で行う。
環境によって操作が異なる部分をsetuptools
モジュールで覆うことで隠してしまおう、という理由でsetup.py
を書く。
from setuptools import setup, Extension from Cython.Build import cythonize name = 'my_module_compiled' # 実際に利用するモジュールの名前 sources = [ # コンパイルするソースファイルの列挙 'my_module_cy.pyx', './src/my_module_c.cpp' ] extra_compile_args = [] # コンパイラオプション ext = Extension( name, sources=sources, language='c++', # 変数不可(?) extra_compile_args=extra_compile_args ) setup( ext_modules = cythonize(ext, language_level="3"), # python3用にコンパイル include_dirs = ['./include/'] # ヘッダの在りかを指定 )
実行
# -i オプションでカレントディレクトリにバイナリがコピーされる $ python setup.py build_ext -i
実行結果
... ライブラリ build\temp.win-amd64-3.9\Release\./src\my_module_compiled.cp39-win_amd64.lib とオブジェクト build\temp.win-amd64-3.9\Release\./src\my_module_compiled.cp39-win_amd64.exp を作成中 コード生成しています。 コード生成が終了しました。 copying build\lib.win-amd64-3.9\my_module_compiled.cp39-win_amd64.pyd ->
Pythonでラップ
型アノテーション書いたり、DocString書いたり。
なくても良い。
保守性を考えるとない方が良いが、利便性を考えるとあった方が良い。
# my_module_py.py import my_module_compiled as mmcy def sum_py(data: list[float]) -> float: return mmcy.sum_cy(data) class MyClass_py(mmcy.MyClass_cy): # 継承可能 def __init__(self, name: str) -> None: super().__init__(name) def print_count(self) -> None: super().print_count()
Cython
で書いた__cinit__
も当然継承される。
引数を*args, **kwargs
としたのはこのため。
簡単なテスト
# my_module_py_test.py import my_module_py as mmpy def main(): data = [3.3, 4.7, 5] total = mmpy.sum_py(data) print(' + '.join(list(map(str, data))), '=', total) obj = mmpy.MyClass_py('Bob') for _ in range(3): obj.print_count() if __name__ == '__main__': main()
実行結果
3.3 + 4.7 + 5 = 13.0 I'm Bob. My count is 0. I'm Bob. My count is 1. I'm Bob. My count is 2.