Boost :: Pythonを使用したC ++アプリケーションへのPythonの埋め込み:パート1
翻訳:Leon Lee([email protected])
オリジナル:こちら
この一連のチュートリアルの紹介では、PythonコードをGranolaコードベースに統合する動機について説明しました。つまり、Python言語と標準ライブラリの利点を利用して、C ++では通常は面倒または不器用なタスクを実行できます。もちろん、肝心なのは、既存のC ++コードを移植する必要がないということです。
今日は、boost :: pythonを使用してPythonをC ++に埋め込み、Pythonオブジェクトと対話する基本的な手順を見ていきます。この部分のすべてのコードをgithubリポジトリに配置しました。コードを確認して、使用してください。
Pythonコアから、Pythonの埋め込みは非常に簡単で、C ++コードは必要ありません。Pythonリリースで提供されるライブラリにはCバインディングコンテンツが含まれています。これらすべてをスキップし、boost :: pythonを介してC ++でPythonを直接使用します。クラスのパッケージ化とポリモーフィックな動作を提供します。Cバインディングと比較して、実際のPythonコードとの整合性が高くなります。このチュートリアルの後半で、 boost :: pythonでは実行できないいくつかのことを紹介します(特にマルチスレッドとエラー処理)
まず、ブーストをダウンロードしてビルドするか、パッケージマネージャーでコピーを取得する必要があります。ビルドすることを選択した場合、boost :: pythonライブラリ(残念ながらヘッダーファイルだけではありません)のみをビルドできますが、C ++プログラミングを頻繁に使用する場合は、Boostライブラリ全体に精通することをお勧めします。上記のgitリポジトリを同期した場合は、Makefile内のブーストインストールディレクトリへのパスを指定してください。さて、続けましょう。
まず、Pythonを組み込んだアプリケーションを構築できる必要があります。gccを使用することはそれほど難しくありません。静的または共有ライブラリとして、boost :: pythonとlibpythonが含まれているだけです。ブーストの構築方法によっては、さまざまな問題が発生する可能性があります。githubのチュートリアルコードでは、静的なboost :: pythonライブラリ(libboost_python.a)と動的バージョンのPythonライブラリ(libpython.so)を使用しています。
MiserWareでの私の開発作業のソフト要件の1つは、サポートするすべてのオペレーティングシステム(一部のWindowsおよび一連の変更されるLinuxディストリビューション)の一貫性を保つことです。したがって、Granolaは固定のPythonバージョンにリンクされており、インストールされたバージョンには、コードの実行に必要なPythonライブラリファイルが含まれています。理想的ではないかもしれませんが、サポートされているすべてのオペレーティングシステムでコードが実行されると確信できる環境を提供します。
いくつかのコードを実行してみましょう。ご想像のとおり、正しいヘッダーファイルをインクルードする必要があるかもしれません。
Py_Initialize();
py::object main_module = py::import(“__ main__”);
py::object main_namespace = main_module.attr(“__ dict__”);
Pythonインタープリターを直接初期化する必要があることに注意してください(最初の行)。boost :: pythonはPythonの埋め込みタスクを大幅に簡素化しますが、必要なすべてのことを処理できるわけではありません。前述のように、次のチュートリアルではさらに多くの欠陥が見られます。初期化後、__ main__モジュールがインポートされ、名前空間が解析されます。これにより、空白のランタイム環境が作成されます。Pythonコードを呼び出して、モジュールと変数を追加できます。
boost::python::exec("print 'Hello, world'", main_namespace);
boost::python::exec("print 'Hello, world'[3:5]", main_namespace);
boost::python::exec("print '.'.join(['1','2','3'])", main_namespace);
exec
この関数は、指定された名前空間の文字列パラメーターのコードを実行します。通常のインポートされていないコードはすべて問題ありません。もちろん、モジュールをインポートして値を抽出することはできないため、あまり役に立ちません。
boost::python::exec("import random", main_namespace);
boost::python::object rand = boost::python::eval("random.random()", main_namespace);
std::cout << py::extract<double>(rand) << std::endl;
ここでは、名前空間__main__
で対応するPythonステートメントを実行してrandom
モジュールをインポートし、このモジュールをこの名前空間に取り込みます。モジュールが利用可能になると、この名前空間で関数、オブジェクト、および変数を使用できます。この例ではeval
、渡されたPythonステートメントの実行結果を返す関数を使用して、random
モジュールのrandom()
関数によって返されるランダムな値を含むboost :: pythonオブジェクトを作成しました。最後に、値をC ++double
タイプとして 抽出し、出力します。
これは少し...ソフトに見えるかもしれません。フォーマットされたPython文字列をC ++関数に渡してPythonを呼び出しますか?これは、トランザクションを処理するためのオブジェクト指向の方法ではありません。幸いなことに、より良い方法があります。
boost::python::object rand_mod = boost::python::import("random");
boost::python::object rand_func = rand_mod.attr("random");
boost::python::object rand2 = rand_func();
std::cout << boost::python::extract(rand2) << std::endl;
この最後の例では、random
モジュールをインポートしましたが、今回はimport
、モジュールをboostpythonのオブジェクトにロードするboost :: pythonの関数を使用します。次に、random
関数オブジェクトrandom
がモジュールから抽出され、boost :: pythonオブジェクトに格納されます。この関数を呼び出して、乱数を含むPythonオブジェクトを返します。最後に、double値が抽出され、出力されます。一般に、すべてのPythonオブジェクトは、関数、クラス、組み込み型など、この方法で処理できます。
複雑な標準ライブラリオブジェクトとユーザー定義クラスのインスタンスを保持し始めると、それは興味深いものになり始めます。次のチュートリアルでは、ConfigParserモジュールを中心に実際の構成解析クラスを構築して、C ++コードからのPython例外の解析について説明します。
Boost :: Pythonを使用したC ++アプリケーションへのPythonの埋め込み:パート2
ではパート1、我々は、アプリケーションからのPythonコードを呼び出すには、いくつかの方法を含め、C ++アプリケーションでのPythonを埋め込む方法を学びました。パート2で完全な構成パーサーを実装することを約束しましたが、エラー解決を検討する方が建設的だと思います。Pythonコードのエラーを処理する良い方法ができたら、パート3で約束の構成パーサーを作成します。始めましょう!
このチュートリアルのgitリポジトリのコピーを入手して使用している場合は、boost :: pythonがPythonエラーを処理する方法(error_already_set例外タイプ)を経験したことがあるかもしれません。そうでない場合、次のコードは例外を生成します。
namespace py = boost::python;
...
Py_Initialize();
...
py::object rand_mod = py::import("fake_module");
…その出力はそれほど有用ではありません:
terminate called after throwing an instance of 'boost::python::error_already_set'
Aborted
つまり、boost::python
処理されたPythonコードでエラーが発生すると、ライブラリはこの例外をスローします。残念ながら、例外はエラー自体に関する情報をカプセル化しません。エラーに関する情報を抽出するには、Python CAPIとPython自体のいくつかのメカニズムを使用する必要があります。まず、エラーをキャッチします。
try{
Py_Initialize();
py::object rand_mod = py::import("fake_module");
}catch(boost::python::error_already_set const &){
std::string perror_str = parse_python_exception();
std::cout << "Error in Python: " << perror_str << std::endl;
}
ここでは、parse_python_exception
関数を呼び出してエラー文字列を抽出し、出力します。示されているように、例外データは、例外自体にカプセル化されるのではなく、Pythonライブラリに静的に格納されます。parse_python_exception
関数の最初のステップは、Python CAPIPyErr_Fetch
の関数を使用してデータを抽出することです。
std::string parse_python_exception(){
PyObject *type_ptr = NULL, *value_ptr = NULL, *traceback_ptr = NULL;
PyErr_Fetch(&type_ptr, &value_ptr, &traceback_ptr);
std::string ret("Unfetchable Python error");
...
異常なデータが全部、一部、またはまったくない可能性があるため、フォールバック値を使用して返される文字列を設定します。次に、例外情報から型データを抽出して文字列化しようとします。
...
if(type_ptr != NULL){
py::handle<> h_type(type_ptr);
py::str type_pstr(h_type);
py::extract<std::string> e_type_pstr(type_pstr);
if(e_type_pstr.check())
ret = e_type_pstr();
else
ret = "Unknown exception type";
}
...
このブロックでは、最初に型データへの有効なポインターが本当にあるかどうかを確認します。存在する場合boost::python::handle
は、データへのポインタを作成し、str
そこからオブジェクトを作成します。この変換により、有効な文字列抽出を確実に実行できるようになりますが、ダブルチェックを実行するには、抽出オブジェクトを作成し、オブジェクトをチェックして、有効な場合は抽出を実行します。それ以外の場合は、フォールバック文字列をタイプ情報として使用します。
次に、外れ値に対して非常によく似た手順を実行します。
...
if(value_ptr != NULL){
py::handle<> h_val(value_ptr);
py::str a(h_val);
py::extract<std::string> returned(a);
if(returned.check())
ret += ": " + returned();
else
ret += std::string(": Unparseable Python error: ");
}
...
既存のエラー文字列に値文字列を追加します。ほとんどの組み込み例外タイプの場合、値文字列はエラーを説明する読み取り可能な文字列です。
最後に、バックトラッキングデータを抽出します。
if(traceback_ptr != NULL){
py::handle<> h_tb(traceback_ptr);
py::object tb(py::import("traceback"));
py::object fmt_tb(tb.attr("format_tb"));
py::object tb_list(fmt_tb(h_tb));
py::object tb_str(py::str("\n").join(tb_list));
py::extract<std::string> returned(tb_str);
if(returned.check())
ret += ": " + returned();
else
ret += std::string(": Unparseable Python traceback");
}
return ret;
}
バックトラッキングは、タイプと値の抽出に似ていますが、バックトラッキングオブジェクトを文字列としてフォーマットするという追加の手順が異なります。このために、traceback
モジュールをインポートします。からtraceback
、format_tb
関数を抽出し、handletracebackオブジェクトを使用して呼び出します。これにより、トレースバック文字列のリストが生成され、それらを1つの文字列に連結します。おそらく最も美しい出力ではありませんが、それは仕事をします。最後に、上記のようにC ++文字列型を抽出し、返されたエラー文字列に追加して、結果全体を返します。
前のエラーのコンテキストで、アプリケーションは次の出力を生成します。
Error in Python: : No module named fake_module
上で述べたように、パート3では、ConfigParserPythonモジュールの上に構築された構成パーサーの実装について説明します。もちろん、私が二度と迷わないことを前提としています。
一般的に、この関数を使用すると、Pythonコードに埋め込まれている問題の根本原因を簡単に見つけることができます。組み込みインタープリター用にカスタムPython環境(特にモジュールパス)を構成しているparse_python_exception
場合、traceback
モジュールを読み込もうとすると関数自体がboost::error_already_set
例外をスローする可能性があるため、関数呼び出しをtry...catch
Blockにラップして、型を解析することをお勧めします。結果の値ポインタ。
前述のように、パート3ではConfigParserPython
、モジュールの上に構築された構成パーサーの実装を紹介します。もちろん、私が再び中断しなかったと仮定します。
Boost :: Pythonを使用したC ++アプリケーションへのPythonの埋め込み:パート3
で、このチュートリアルのパート2、私が埋め込まれたPythonコードによってスローされたハンドルの例外へのアプリケーションのC ++コードを使用する方法を紹介しました。これは、埋め込まれたPythonコードをデバッグするために不可欠です。このチュートリアルでは、Python機能を使用して、実用的なアプリケーションの開発でしばしば厄介な部分である構成分析を処理する単純なC ++クラスを作成します。
C ++エリートを怒らせないために、私はこれを外交的に言います:私はC ++で複雑な文字列操作を使用します。STLstrings
とSTLは stringstreams
タスクを大幅に簡素化しますが、アプリケーションレベルのタスクを実行し、それらを堅牢な方法で実行すると、常により多くのコードを作成することになります。そのため、最近、組み込みPython、特にConfigParser
モジュールを使用して、Granola Connect(Granola RESTAPIとの通信を処理するGranolaEnterpriseのデーモン)の構成分析メカニズムを書き直しました。
もちろん、文字列の操作と構成の分析はほんの一例です。パート3では、処理が難しいがPythonでの単純なタスク(Web接続など)をいくつでも選択できますが、構成解析クラスは、Pythonを実用化するための単純ですが完全な例です。このチュートリアルのコードは、Githubリポジトリから入手してください。
まず、非常に基本的な構成解析をカバーするクラス定義を作成しましょう。INIスタイルのファイルを読み取って解析し、指定された名前とセクションの文字列値を抽出し、指定されたセクションの文字列値を設定します。これはクラス宣言です:
class ConfigParser{
private:
boost::python::object conf_parser_;
void init();
public:
ConfigParser();
bool parse_file(const std::string &filename);
std::string get(const std::string &attr,
const std::string §ion = "DEFAULT");
void set(const std::string &attr,
const std::string &value,
const std::string §ion = "DEFAULT");
};
このConfigParser
モジュールで提供される機能は、このチュートリアルで説明されている機能をはるかに超えていますが、ここで実装するサブセットは、より複雑な機能のテンプレートとして使用する必要があります。このクラスの実装は非常に単純です。最初に、コンストラクターは__main__
モジュールをロードし、ディクショナリを抽出し、ConfigParser
モジュールを名前空間にインポートしboost::python::object
、RawConfigParser
オブジェクトを含むメンバー変数のタイプを作成します。
ConfigParser::ConfigParser(){
py::object mm = py::import("__main__");
py::object mn = mm.attr("__dict__");
py::exec("import ConfigParser", mn);
conf_parser_ = py::eval("ConfigParser.RawConfigParser()", mn);
}
次のconfig_parser_
オブジェクトを使用して、ファイルの解析と値の取得および設定を実行します。
bool ConfigParser::parse_file(const std::string &filename){
return py::len(conf_parser_.attr("read")(filename)) == 1;
}
std::string ConfigParser::get(const std::string &attr, const std::string §ion){
return py::extract<std::string>(conf_parser_.attr("get")(section, attr));
}
void ConfigParser::set(const std::string &attr, const std::string &value, const std::string §ion){
conf_parser_.attr("set")(section, attr, value);
}
この簡単な例では、簡潔にするために、例外を伝播することが許可されています。より複雑な環境では、ほぼ確実に、C ++クラスでPython例外をC ++例外として処理および再パッケージ化する必要があります。パフォーマンスやその他の問題が問題になる場合は、後で純粋なC ++クラスを作成できます。
このクラスを使用するために、呼び出し元のコードは単純に通常のC ++クラスとして扱うことができます。
int main(){
Py_Initialize();
try{
ConfigParser parser;
parser.parse_file("conf_file.1.conf");
cout << "Directory (file 1): " << parser.get("Directory", "DEFAULT") << endl;
parser.parse_file("conf_file.2.conf");
cout << "Directory (file 2): " << parser.get("Directory", "DEFAULT") << endl;
cout << "Username: " << parser.get("Username", "Auth") << endl;
cout << "Password: " << parser.get("Password", "Auth") << endl;
parser.set("Directory", "values can be arbitrary strings", "DEFAULT");
cout << "Directory (force set by application): " << parser.get("Directory") << endl;
// Will raise a NoOption exception
// cout << "Proxy host: " << parser.get("ProxyHost", "Network") << endl;
}catch(boost::python::error_already_set const &){
string perror_str = parse_python_exception();
cout << "Error during configuration parsing: " << perror_str << endl;
}
}
それだけです。ブロックとコメントを含むKey-Value構成パーサーには、50行のコードしか必要ありません。これは氷山の一角にすぎません。ほぼ同じ長さのコードで、さまざまなことを実行できます。これらはC ++で最も苦痛であり、エラーが発生しやすく、時間がかかります。構成の解析、リストと収集の操作、Web接続、ファイル形式の操作(考えてみてください) XML / JSON)、およびPython標準ライブラリに実装されている他の無数のタスク。
でパート4、私はより強力かつ普遍的にPythonコードを呼び出すためにファンクタやPythonの名前空間のクラスを使用する方法を紹介します。
Boost :: Pythonを使用したC ++アプリケーションへのPythonの埋め込み:パート4
このチュートリアルのパート2では、C ++からのPython例外を解析するために使用されるコードを紹介しました。でパート3、私はConfigParser
シンプルな構成の解析クラスを実装し、Pythonの使用 モジュールを。この実装の一環として、あらゆるサイズのプロジェクトで、クラスの顧客がPythonの詳細を理解する必要がないように、クラスでPythonの例外をキャッチして処理したいと述べました。呼び出し元の観点からは、このクラスは他のC ++クラスと同じです。
Python例外を処理する明白な方法は、すべての関数でそれらを処理することです。たとえば、作成したC ++ ConfigParserクラスのget関数は次のようになります。
std::string ConfigParser::get(const std::string &attr, const std::string §ion)
{
try{
return py::extract(conf_parser_.attr("get")(section, attr));
}catch(boost::python::error_already_set const &){
std::string perror_str = parse_python_exception();
throw std::runtime_error("Error getting configuration option: " + perror_str);
}
}
エラー処理コードは同じままですが、main
関数は次のようになります。
int main()
{
Py_Initialize();
try
{
ConfigParser parser;
parser.parse_file("conf_file.1.conf");
...
// Will raise a NoOption exception
cout << "Proxy host: " << parser.get("ProxyHost", "Network") << endl;
}catch(exception &e){
cout << "Here is the error, from a C++ exception: " << e.what() << endl;
}
}
Python例外がスローされると、解析されて1つstd::runtime_error
に再パッケージ化されます。これは、呼び出しサイトでキャッチされ、通常のC ++例外のように処理されます(つまり、parse_python_exception
厳密な操作は必要ありません)。組み込みPythonを使用する関数が少ないプロジェクトまたは1つまたは2つのクラスのプロジェクトの場合、これは間違いなく機能します。ただし、大規模なプロジェクトの場合、多くの繰り返しコードを避けたいと考えています。これにより、必然的にエラーが発生します。
私の実装では、常に同じ方法でエラーを処理したいのですが、異なるシグネチャで異なる関数を呼び出す方法が必要です。私boost
はライブラリの別の強力な領域を利用することにしました:ファンクターライブラリ、特にboost::bind
とboost::function
。関数パラメーターboost::function
をboost::bind
バインドするためのファンクターラッパーを提供します。次に、この2つを組み合わせることで、後で呼び出すことができる関数とそのパラメーターを渡すことができます。まさに医者が求めたものです!
ファンクターを使用するには、関数が戻り値の型を知っている必要があります。さまざまな署名ラッピング関数を使用しているため、関数テンプレートはこれをうまく行うことができます。
template <class return_type>
return_type call_python_func(boost::function<return_type ()> to_call, const std::string &error_pre)
{
std::string error_str(error_pre);
try{
return to_call();
}catch(boost::python::error_already_set const &)
{
error_str = error_str + parse_python_exception();
throw std::runtime_error(error_str);
}
}
この関数は、boost::python
関数を呼び出す関数としてファンクターオブジェクトを使用します。boost::python
コードを呼び出す各関数は、2つの関数に分割されました。プライベートコア関数はPython関数を呼び出し、パブリックラッパー関数は関数を使用しcall_python_func
ます。これは、更新されたget
関数とそのパートナーです。
string ConfigParser::get(const string &attr, const string §ion)
{
return call_python_func<string>(boost::bind(&ConfigParser::get_py, this, attr, section), "Error getting configuration option: ");
}
string ConfigParser::get_py(const string &attr, const string §ion)
{
return py::extract<string>(conf_parser_.attr("get")(section, attr));
}
get
関数は、渡されたパラメータとの暗黙のthisポインタバインドget_py
機能、およびget_py·函数又调用
ブースト:: python`が実行に必要な機能を。シンプルで効果的。
もちろん、ここにはトレードオフがあります。try ... catchブロックコードとPythonエラー処理を繰り返す代わりに、各クラスで宣言される関数の数が2倍になりました。私の目的では、エラーを見つけるためにコンパイラーをより効率的に使用するため、2番目の形式を好みますが、長さは異なる場合があります。最も重要なポイントは、Pythonコードを理解するレベルでPythonエラーを処理することです。アプリケーション全体がPythonを理解する必要がある場合は、埋め込みではなくPythonで書き直すことを検討する必要があります。おそらく、必要に応じていくつかのC ++モジュールを使用します。
いつものように、githubリポジトリのクローンを作成することでこのチュートリアルを完了することができます。