Python
C++
boost
Python3

Embedded Python in C++ / C++からPythonスクリプトを呼び出す

More than 3 years have passed since last update.

この記事は札幌C++勉強会のオンラインもくもく会で書きました。

札幌C++勉強会では、もくもく会の参加者を募集しております。

詳しくは@ignis_fatuusまで


概要

C++ですべてをやるのはつらい。だから部分的にPythonで楽をする。Boost.Python使えばできる。試しにTwetter APIでも使ってみた。


Background

Pythonは便利。PythonでGUIなアプリを作りたい事情が筆者にあるが、困った問題がある。QtとPythonのbindingライブラリだ。


GUIライブラリのPython Bindings

PythonでGUIを使うにはQtやGtkなどの、C言語またはC++用のGUIライブラリを使う。これら他言語のライブラリをPythonから呼び出す仕組みをbindingsと呼んだりする。Qtはとても有名なC++用のGUIライブラリで、これ選んでおけばあまり大きくハズレはしない、これを使いたい。Gtkやその他の選択肢もあるが、まあQtが無難。


PythonのQt bindings

PyQtPySideがある。Qtの最新版(2016/3/31現在)であるQt5を扱うにはPyQt5を利用すればいい。しかしPyQt5はGPLもしくは商用ライセンスなので避けたい。一方PySideは、最新の安定リリース版(2016/3/31時点)であるPySide 1.2.4はQt4.8までにしか対応していない。PySide2ではQt5に対応するようだが、現時点ではWindowsでのビルドに失敗するようだ。


GUIの部分はC++でやる。

以上のように、Pythonから使えて、筆者が納得できるGUIライブラリが現時点で存在しない。しかし、QtはもともとC++ライブラリなので、最初からC++で書けばフルに機能を使える。ところが全部をC++で書こうとすると非常にめんどくさい。従って、C++で書かねばならぬところ(今回のケースだとGUIの部分)はC++で書き、内部の大してパフォーマンスも要求されない部分はPythonで書く。例えばネットワーク通信はPythonでやる。なぜならC++でパフォーマンスを上げてもネットワーク遅延があるので大した利点がない。


この記事で説明すること

Boost.Pythonを使ってC++からPythonを呼び出す方法を説明する。例として、Twitterのタイムラインを取得し、C++で標準出力に出すプログラムを紹介する。タイムラインの取得にはPythonのライブラリ、tweepyを利用する。


インストール


C++、Qt、Python、Boost.Python

WindowsのMsys2環境であればpacmanでインストールできる。

$ pacman -S mingw-w64-x86_64-gcc

$ pacman -S mingw-w64-x86_64-qt5
$ pacman -S mingw-w64-x86_64-python3
$ pacman -S mingw-w64-x86_64-gcc


tweepy

$ pip3 install tweepy

もしかしたら、python3 -m ensurepip --default-pipとか必要だったかもしれない。pyvenv、virtualenvなどを使いたい場合は使っても問題ない。


Pythonスクリプト

C++から呼び出されるPythonコードを書く。ファイル名はembed.pyとする。


embed.py

#!/usr/bin/env python3

#vim:fileencoding=utf8
import sys
# pyvenvやAnacondaなどで、通常とは別の場所にtweepyをインストールした場合はその場所を指定する。
# sys.path.append('C:/Anaconda3/envs/MyEnvironments/Lib/site-packages')

# tokenはTwitterから取得しておく。
consumer_key = "...."
consumer_secret = "...."
access_token = "...."
access_token_secret = "...."

# tweepyの使い方は他の記事や公式サイトを参考にされたし
import tweepy
auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_token, access_token_secret)
api = tweepy.API(auth)

# この関数をC++から呼び出す
def timeline():
public_tweets = api.home_timeline()
tweets = [ (tweet.author.name, tweet.text) for tweet in public_tweets ]
return tweets


まず最初に、tweepyやその依存ライブラリがインストールされているディレクトリを追加する。これはAnacondaやpyvenvなどを使っている場合は必要だが、使ってない場合は必要ないかもしれない。Twitter APIを利用するために必要なトークンは、Twitterサイトで開発者の登録をして申請して取得しておく。tweepyについては特に説明しない。最後に定義したtimelineがC++から呼び出す関数である。今回は、ツイート主の名前とツイート内容をタプルにし、そのリストを返すことでタイムラインのデータとした。


C++ソースを用意


twitter.cpp

#include<iostream>

#include<string>
#include<fstream>
#include<streambuf>
#include<boost/python.hpp>

# Pythonスクリプトでの(tweet.author.name, tweet.text)というタプル相当の構造体
struct Status {
std::string name;
std::string text;
Status(const boost::python::tuple t) :
name(boost::python::extract<std::string>(t[0])),
text(boost::python::extract<std::string>(t[1]))
{ }
};

int main() {
// PYTHONHOME環境変数を設定、Py_Initialize()の前に呼び出す
// 環境によってはいらないのかもしれない。
wchar_t python_home[] = L"c:\\msys64\\mingw64"; // python2ではcharでよい。python3だとwchar_t。
Py_SetPythonHome(python_home);

Py_Initialize();
auto main_mod = boost::python::import("__main__");
auto main_ns = main_mod.attr("__dict__");

// read python script
std::ifstream ifs("embed.py");
std::string script((std::istreambuf_iterator<char>(ifs)),
std::istreambuf_iterator<char>());
// execute python script,
// but not invoke timeline() function (definition only)
boost::python::exec(script.c_str(), main_ns);

// call python's timeline function and get the timeline data
using input_iterator = boost::python::stl_input_iterator<boost::python::tuple>;
auto timeline_data = main_mod.attr("timeline")();
input_iterator begin(timeline_data), end;

std::vector<Status> tweets(begin,end);
for(auto tweet: tweets) {
std::cout << tweet.name << ":" << tweet.text << std::endl;
}
}


もくもく会の時間が尽きたので、簡単に説明する。

一番のポイントは「Pythonからどのようにデータを受け取るか」。これはBoost.PythonではIteratorを使って簡単にできる。それが

    using input_iterator = boost::python::stl_input_iterator<boost::python::tuple>;

auto timeline_data = main_mod.attr("timeline")();
input_iterator begin(timeline_data), end;
std::vector<Status> tweets(begin,end);

の部分。誠に申し訳ないがここでタイムアップである。

ソースを読んで察して欲しい。

追記:

ビルド方法の例だけ残す

g++ -o twitter.exe twitter.cpp -std=c++14 -I/mingw64/include/python3.5m -lboost_python3-mt -lpython3.5m