Python ライブラリを Ruby から使う
English version is available at github
PyCall は Ruby の中から既存の Python ライブラリを使うにはとても便利なライブラリです。 とはいえ、これは Ruby から Python の関数を呼ぶためのライブラリなので、Python の関数を1回呼ぶ度に Ruby に制御が戻ってきます。 これが元でちょっと間違えやすいこともあるようです。 例えば下のような記事を見つけました。
Ruby×PyCallでTensorflowのMNISTチュートリアル「連想配列の違いで手間取った」
そこで Ruby の柔軟な構文を活かして、Python っぽいコードを Ruby プログラムの中に埋め込んでおくと、そこだけまとめて切り出して Python に送って実行する DSL を作ってみました。 Yadriggy を使って作っています。 この DSL の実装は内部で PyCall を使っていますから、要は PyCall のフロントエンドです。 なるべく Python で書かれたサンプルプログラムをそのまま Ruby プログラムの中にコピーして動くように DSL を設計してみました。
準備
まず yadriggy をインストールしてください。バージョン 1.2.0 以上が必要です。
gem install yadriggy
当然、Python が必要です。Python 2 でも 3 でも動くはずです。
PyCall は標準では python
コマンドで Python
インタプリタを起動するようですが、違うコマンドにする場合は環境変数をセットします。
例えば
export PYTHON=python3
のようにです。 また使いたい Python のライブラリもインストール済みでなければなりません。
例題
この DSL を使ったプログラム例を示します。埋め込み DSL なので、以下は Ruby のプログラムです。
require 'yadriggy/py'
# draw_pie() は Python の関数
def draw_pie(labels, sizes, explode)
fig1, ax1 = plt.subplots()
ax1.pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%',
shadow=True, startangle=90)
ax1.axis('equal')
plt.show()
end
def run()
# 以下 3 行は Ruby のコード
Yadriggy::Py::Import::import('matplotlib.pyplot').as(:plt)
labels = 'Frogs', 'Hogs', 'Dogs', 'Logs'
sizes = [15, 30, 45, 10]
Yadriggy::Py::run do
# このブロックの内部は Python のコード
ex = tuple(0, 0.1, 0, 0) # tuple が必要
draw_pie(labels, sizes, ex)
end
end
run
(この例題は Matplotlib のチュートリアルから引用しました。)
上のプログラムの中で DSL で書かれたコードなのは、Yadriggy::Py::run
の引数ブロックの中の
# このブロックの内部は Python のコード
ex = tuple(0, 0.1, 0, 0) # tuple が必要
draw_pie(labels, sizes, ex)
この部分と、この中で呼ばれている draw_pie
メソッドの定義
# draw_pie() は Python の関数
def draw_pie(labels, sizes, explode)
fig1, ax1 = plt.subplots()
ax1.pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%',
shadow=True, startangle=90)
ax1.axis('equal')
plt.show()
end
です。
プログラムが実行されて Yadriggy::Py::run
メソッドが呼ばれると、引数の
DSL コードは Python 側へ送られて実行されます。
draw_pie
メソッドは Yadriggy::Py::run
の引数のブロックの中のから呼ばれているので、DSL
コードの一部と見なされて一緒に Python 側へ送られます。
これらに加えて、変数 labels
, sizes
, ex
の値もコピーされて Python 側へ送られます。
同様に Yadriggy::Py::run
の引数のブロックの中で参照されているからです。
draw_pie
メソッドの定義は Ruby の構文を使って書かれていますが、あくまで DSL
のコードですので、Python のコードとして解釈されます。
例えば ax1.pie()
の引数の explode=explode
は Python のキーワード引数として解釈されます。
実行
上記のプログラムが matplotlib.rb
とすると、
ruby matplotlib.rb
のように実行すれば円グラフが表示されます。
ちょっと便利… でしょうか?
Python との違い
対応する元の Python プログラムは
import matplotlib.pyplot as plt
def draw_pie(labels, sizes, explode):
fig1, ax1 = plt.subplots()
ax1.pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%',
shadow=True, startangle=90)
ax1.axis('equal')
plt.show()
def run():
labels = 'Frogs', 'Hogs', 'Dogs', 'Logs'
sizes = [15, 30, 45, 10]
ex = (0, 0.1, 0, 0) # tuple は不要
draw_pie(labels, sizes, ex)
run()
です。
DSL コードと比較すると、違いは変数 ex
の初期値の tuple を作るとき、DSL コードでは
tuple(...)
と書かなければならないことと、import
文がメソッド呼び出しの鎖になっていることぐらいです。
Import 文は
Yadriggy::Py::Import::import('matplotlib.pyplot').as(:plt)
となっています。
それからメソッド(関数)定義が def
… end
の組になっているところも Python とは異なります。
結構似るように DSL を作ったつもりですが、まったく同じ、というわけではありません。
このように、いかに Ruby の構文が柔軟とはいっても、Python コードそのままでは構文エラーになってしまう部分が少なからずあります。 このため、任意の Python コードをそのままコピーして Ruby の中に埋め込めんでも動く、というわけにはいきません。 詳しくは Wiki を見てください。
String embedding と同じでは?
PyCall には PyCall::exec
や PyCall::eval
といったメソッドがあり、文字列で渡した
Python コードを実行してくれます。
実は本 DSL も、最終的にはこれらのメソッドを使って Python のコードを実行しています。
なんだ PyCall::exec
等があれば十分だったではないか、と思うかも知れませんが、本 DSL
を使うとコード中で参照されている変数の値も一緒に Python へコピーされます。
例えば
def run()
Yadriggy::Py::Import::import('matplotlib.pyplot').as(:plt)
labels = 'Frogs', 'Hogs', 'Dogs', 'Logs'
sizes = [15, 30, 45, 10]
PyCall::exec <<CODE # コードを文字列として渡すので string embedding
ex = (0, 0.1, 0, 0)
draw_pie(labels, sizes, ex)
CODE
end
一見よさそうに見えますが、このようにすると、変数 labels
, sizes
, ex
の値は Python
側へコピーされません。
おそらくそれらの変数が Python では未定義であるというエラーになるでしょう。
本 DSL ではそれらの変数の値もコピーされますので、エラーにはなりません。
これって役に立つの?
この DSL の作っているとき、周囲に話をすると、そんな DSL 何の役に立つのか、とよく聞かれました。 実用性という観点からは、PyCall があれば十分かもしれません。 が、Ruby プログラムの中に Python っぽいプログラムが同居していて、目的に合わせて適した方を使う、というのも悪くないと思いませんか? … 思わないか。