3 PyCallを使ってみる(1)
ではPyCallを実際に使ってみましょう。はじめに本稿執筆時点でのインストール方法を説明します。それから、前説で紹介したPyCallの基本機能を試します。最後に、Pythonのデータ分析用ライブラリをPyCall経由で利用して実際のデータを分析します。
3.2 準備
3.2.1 PyCallのインストール
PyCallはgem installコマンドでインストールできます。まだ安定版をリリースしていないため、gem installコマンドに--preオプションを指定する必要があります。PyCallと同時にmatplotlibのラッパーライブラリもインストールしてください。
$ gem install --pre pycall matplotlib
3.2.3 Pythonのインストール
次にPythonをインストールしましょう。本稿ではPython 3.6.0で動作確認をしています。注意点はただ一つ、共有ライブラリlibpythonをインスールすることです。
Linuxの場合、apt-getコマンドやyumコマンドを用いてPythonをインストールすると共有ライブラリが有効なものがインストールされます。
Anacondaを利用してインストール[7]する場合も、共有ライブラリが有効なPythonがインストールされます。MacOS XやWindowsを利用している方は、Anacondaを利用してPythonをインストールすると良いでしょう。
pyenv[8]を利用したり、自分でソースツリーを展開してビルド[9]したりする場合は、configureに--enable-sharedオプションを指定することを忘れないでください。
3.2.8 Pythonのライブラリ群のインストール
Pythonのインストールが完了したら、次はライブラリ群をインストールしましょう。ライブラリをインストールするためにpipというパッケージ管理システムが必要になります。こちらは次のコマンドでインストールできます。
$ curl -kL https://raw.github.com/pypa/pip/master/contrib/get-pip.py | python
Linuxでapt-getやyumを用いてPythonをインストールした場合は、パイプの後ろのpythonをsudo pythonに置き換える必要があります。
pipをインストールできたら、必要なライブラリをインストールしましょう。本稿では次のライブラリを利用します。
$ pip install matplotlib numpy pandas seaborn
3.3 基本機能の確認
第2節で解説したPyCallの基本機能を実際に試してみましょう。
3.3.2 PyCall.evalと型変換
PyCall.evalを利用して、Pythonコードの評価結果がRubyではどういったオブジェクトに変換されるかを見ていきましょう。次のように、Pythonの整数、浮動小数点数、複素数は、それぞれRubyのInteger、Float、Complexに変換されます。
PyCall.eval('1').class #=> Integer
PyCall.eval('1.1').class #=> Float
PyCall.eval('1 + 1j').class #=> Complex
また、次のようにPythonの文字列はRubyのStringに変換されます。
PyCall.eval('"string"').class #=> String
listはPyCall::Listオブジェクトでラップされ、要素の取り出しなどの基本機能はRubyの配列と同様の自然な記法でサポートされます。
list = PyCall.eval('[1, 2, 3]')
list.class #=> PyCall::List
list.length #=> 3
list[0] #=> 1
list[1] #=> 2
list[3] #=> 3
dictはPyCall::Dictオブジェクトでラップされます。PyCall::DictはHashと似た振る舞いになるように作られています。
dict = PyCall.eval('{ "a": 1, "b": 2, "c": 3 }')
dict.length #=> 3
dict['a'] #=> 1
dict['b'] #=> 2
dict['c'] #=> 3
dict.keys #=> [ 'a', 'b', 'c' ]
dict.values #=> [ 1, 2, 3 ]
さらに、tupleはPyCall::Tupleで、setはPyCall::Setでラップされます。それぞれlistやdictと同様に確認できるのでやってみてください。
また、クラス対応表に登録されていないクラスのインスタンスがPyCall::PyObjectに変換されることを確認してみましょう。
PyCall.eval(<<PYTHON, input_type: :file)
class Hoge:
def ping(self, str):
return "PONG: " + str
PYTHON
hoge = PyCall.eval('Hoge()')
hoge.class #=> PyCall::PyObject
変数hogeが参照するオブジェクトを使い、インスタンスメソッドへのアクセスが関数オブジェクトの取り出しになること、そしてインスタンスメソッドを呼び出すためにはmethod_name.(args, ...)記法を利用する必要があることを確認してみましょう。
hoge.ping #=> PyCall::PyObject
hoge.ping.inspect #=> "<bound method Hoge.fun of …>"
hoge.ping.("hello") #=> "PONG: hello"
このように、ただhoge.pingと書いてもメソッドのオブジェクトが取り出せるだけあり、それを呼び出すためにhoge.ping.(...)と書く必要があります。
次はラッパークラスを作り、インスタンスメソッドを自然に呼び出せるようにしてみましょう。Ruby側にもHogeクラスを作り、PyCall::PyObjectWrapperを利用してラッパークラスにします。
class Hoge
include PyCall::PyObjectWrapper
wrap_class PyCall.eval('Hoge')
end
これだけでラッパークラスの定義とクラス対応表への登録が完了しています。簡単ですね。
では、PyCall.evalで再度Python側のHogeクラスのオブジェクトを作ってみましょう。
hoge2 = PyCall.eval('Hoge()')
hoge2.class #=> Hoge
このように、wrap_classでラッパークラスを定義すると、Ruby側のクラスがPyCall::PyObjectからHogeに変わりました。Hoge#pingメソッドをRubyの自然な記法で呼び出せるように上書きしてみましょう。
class Hoge
def ping(*args)
super().(*args)
end
end
これで変数hoge2が参照するオブジェクトのpingメソッドをmethod_name.(args, ...)記法を使わずに呼び出せるようになっています。実際に呼び出してみましょう。
hoge2.ping "hi" #=> "PONG: hi"
はい、呼び出せてますね。
このようなラッパーメソッドを手作業で作っていくのは面倒です。そのため、ラッパーメソッドを定義しやすくするヘルパーをPyCallの安定版のリリースまでに追加する予定です。
3.3.3 pyimportとpyfrom
続いてPythonのモジュールをRubyのモジュールにインポートしてみましょう。
require 'pycall/import' module Py extend PyCall::Import pyimport :numpy, as: :np end
これを評価すると、モジュールPyが定義され、PythonのnumpyモジュールをラップしたPyCall::PyObjectオブジェクトを返す特異メソッドnpがモジュールPyに追加されます。Py.npがnumpyであることを確かめてみましょう。
Py.np #=> <module 'numpy' from ...> Py.np.arange.(0, 10) #=> array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
Py.npがnumpyモジュールであることが確認できました。np.arangeを呼び出すためにmethod_name.(args, ...)記法を使う必要があるのは上述の通りです。
3.3.5 PyCall.wrap_ruby_callable
PyCall.wrap_ruby_callableの例として、sorted関数のkeyにRubyのProcオブジェクトを渡す例を示します。
次の例は、Pythonのsorted関数を使い、1から9までの整数を持つ配列、偶数は符号を変えて負数として、奇数はそのままの値で比較して並び替えます。実行結果は[8, 6, 4, 2, 1, 3, 5, 7, 9]というリストになりますが、並び替えの基準となるキー関数としてRubyのProcオブジェクトを渡している点に注目してください。
ary = [1, 2, 3, 4, 5, 6, 7, 8, 9]
key_func = ->(v) { v.even? ? -v : v }
sorted_list = PyCall.builtin.sorted.(ary, key: PyCall.wrap_ruby_callable(key_func))
p sorted_list #=> [8, 6, 4, 2, 1, 3, 5, 7, 9]
上述の通り、PyCallはProcオブジェクトに対しては自動的にPyCall.wrap_ruby_callableを使ってラッパーを生成するので、この例の3行目は以下の通りに書いても動きます。
sorted_list = PyCall.builtin.sorted.(ary, key: key_func)
