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)

となっています。 それからメソッド(関数)定義が defend の組になっているところも Python とは異なります。 結構似るように DSL を作ったつもりですが、まったく同じ、というわけではありません。

このように、いかに Ruby の構文が柔軟とはいっても、Python コードそのままでは構文エラーになってしまう部分が少なからずあります。 このため、任意の Python コードをそのままコピーして Ruby の中に埋め込めんでも動く、というわけにはいきません。 詳しくは Wiki を見てください。

String embedding と同じでは?

PyCall には PyCall::execPyCall::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 っぽいプログラムが同居していて、目的に合わせて適した方を使う、というのも悪くないと思いませんか? … 思わないか。