去年の8月に RejectKaigi 2017 にて発表した、「PythonのコードをPython ASTベースでRubyに変換を行う py2rb.py」が、初版公開できるレベルになったので、python のパッケージとして PyPIで公開しました。
py2rb.py は、Pythonの機械学習関連のライブラリ、特に Chainer を Ruby に移植するためにPythonからRubyへのトランスコンパイラを作成している位置づけになります。
py2rb.py 開発の経緯の詳細は、下記 blog をご覧ください。
特徴
実行方法
$ py2rb Usage: py2rb.py [options] filename.py or py2rb.py [-w [-f]] [-(r|b)] [-v] filename.py or py2rb.py -p foo/bar/ -m [-w [-f]] [-(r|b)] [-v] foo/bar/filename.py or py2rb -l lib_store_directory/ [-f]
Usage のように変換対象の Python のファイル名を指定して実行すると、変換結果を標準出力に表示します。
(以前は利用しているローカルのモジュール名を個別に指定する必要がありましたが、0.1.0では import 文を解釈しローカルのモジュールファイルを自動的に参照します。)
"-w" オプションをつけると、filename.py の変換結果を filename.rb に拡張子を変えた形式で保存します。
すでに filename.rb が存在すると処理を Skip するので、上書き保存をしたい場合は "-f" オプションをあわせて指定します。
"-m" オプションをつけると参照したローカルのモジュールファイルもあわせて一括変換します。
実際に変換すると下記のようになります。
Python サンプルコード:
$ git clone git://github.com/naitoh/py2rb.git
$ cat py2rb/tests/strings/zipstring.py
s1 = "hello" s2 = "world" s3 = "abcd" s4 = list(zip(s1,s2,s3)) for item in s4: print("----") for val in item: print(val)
Python実行例:
$ python py2rb/tests/strings/zipstring.py
----
h
w
a
----
e
o
b
----
l
r
c
----
l
l
d
変換例:
$ py2rb -r py2rb/tests/strings/zipstring.py
# frozen_string_literal: true require 'module' using EnumerableEx using PythonZipEx using PythonPrintEx using PythonIsBoolEx using PythonIndexEx using PythonFindEx using PythonSplitEx using PythonStripEx using PythonStringCountEx using PythonRemoveEx using PythonMethodEx s1 = "hello" s2 = "world" s3 = "abcd" s4 = zip_p(s1, s2, s3).to_a for item in s4 print("----") for val in item print(val) end end
なお、ここで Python の zip メソッドを Rubyでは独自に定義した zip_p メソッドに置き換えています。
Python と Ruby で互換性のないメソッドなどの場合、互換用ライブラリを使用することで互換性を確保しています。
"using PythonZipEx" をコメントアウトすれば互換用ライブラリのメソッドは個別に無効になるので、処理の置き換えなどを実施後に該当処理が不要になれば using 行を削除するだけで済みます。
先ほどは、標準出力に表示されましたが、"-w" をつけることで、ファイル保存されます。
$ py2rb -r py2rb/tests/strings/zipstring.py -w Try : py2rb/tests/strings/zipstring.py -> py2rb/tests/strings/zipstring.rb : [OK]
"-l" で互換用ライブラリを指定ディレクトリに保存します。今回はカレントディレクトリに保存します。
$ py2rb -l .
OK : ./module.rb file was stored.
Ruby の -I オプションでカレントディレクトリをライブラリパスを追加し、実行時に module.rb を呼び出し可能にして実行します。
Ruby実行例:
$ ruby -I . py2rb/tests/strings/zipstring.rb ---- h w a ---- e o b ---- l r c ---- l l d
Python の実行結果と同じになりました。
大規模なソースコードの変換の例として、chainer の変換例は次のようになります。
$ git clone git://github.com/chainer/chainer.git
$ cat chainer/chainer/variable.py
import collections import copy import heapq import traceback import warnings import weakref import numpy import chainer from chainer.backends import cuda from chainer import initializers from chainer.initializers import constant from chainer.utils import argument def _check_grad_type(func, x, gx): if x.data is None or gx is None: # ``x.data is None`` implies that the data array is not retained return if not isinstance(gx, type(x.data)): msg = ('Type of data and grad mismatch\n%s != %s' % (type(x.data), type(gx))) typ = TypeError elif gx.dtype != x.data.dtype: msg = ('Dtype of data and grad mismatch\n%s != %s' % (x.data.dtype, gx.dtype)) typ = TypeError elif gx.shape != x.data.shape: msg = ('Shape of data and grad mismatch\n%s != %s' % (x.data.shape, gx.shape)) typ = ValueError else: return detail = '' if func: detail = 'Function `{0}` ({1}) has a bug.\n'.format( type(func)._impl_name, func.label) stack = func.stack if stack: detail += 'Stacktrace of the function is below:\n' for line in traceback.format_list(func.stack): detail += line detail += ''' Please report this error to the issue tracker with the stack trace, the information of your environment, and your script: https://github.com/chainer/chainer/issues/new. '''.format(type(func).__name__, func.label) raise typ(detail + msg) (略)
$ py2rb -p chainer chainer/chainer/variable.py
module Chainer module Variable require 'weakref' require_relative 'backends/cuda' include Chainer::Backends require_relative 'chainer' include Chainer require_relative 'initializers/constant' include Chainer::Initializers require_relative 'utils/argument' include Chainer::Utils def _check_grad_type(func, x, gx) if is_bool((x.data === nil)||(gx === nil)) return end if is_bool(!(gx.is_a? (x.data).class)) msg = "Type of data and grad mismatch %s != %s" % [(x.data).class, (gx).class] typ = ArgumentError else if gx.dtype != x.data.dtype msg = "Dtype of data and grad mismatch %s != %s" % [x.data.dtype, gx.dtype] typ = ArgumentError else if gx.shape != x.data.shape msg = "Shape of data and grad mismatch %s != %s" % [x.data.shape, gx.shape] typ = TypeError else return end end end detail = "" if is_bool(func) detail = "Function `{0}` ({1}) has a bug. ".format((func).class._impl_name, func.label) stack = func.stack if is_bool(stack) detail += "Stacktrace of the function is below: " for line in (func.stack).to_a detail += line end end detail += " Please report this error to the issue tracker with the stack trace, the information of your environment, and your script: https://github.com/chainer/chainer/issues/new. ".format((func).class.__name__, func.label) end raise typ, (detail)+(msg) end (略)
ここまでの規模になると、ソースコード修正が必要になりますが、大部分の機械的な置き換えが行われた後なので悩む箇所や手間が削減されます。
実際にPython からの移植を行う場合は、"-m" オプションをつけると "-p" のパス配下のファイルで指定されたpython ファイル (下記の場合は chainer/chainer/variable.py) から参照されているファイルを一括変換するので便利です。
$ py2rb -p chainer chainer/chainer/variable.py -m -w Try : chainer/chainer/functions/math/fmod.py -> chainer/chainer/functions/math/fmod.rb : [OK] Try : chainer/chainer/functions/array/expand_dims.py -> chainer/chainer/functions/array/expand_dims.rb : [OK] Try : chainer/chainer/functions/pooling/average_pooling_nd.py -> chainer/chainer/functions/pooling/average_pooling_nd.rb : [OK] Warning : syntax not supported (<_ast.ExtSlice object at 0x102df11d0>) Warning : ExtSlice not supported (path[]) in Assign(ast.Subscript) Warning : syntax not supported (<_ast.ExtSlice object at 0x102df11d0>) Warning : syntax not supported (<_ast.ExtSlice object at 0x102dd35f8>) Warning : syntax not supported (<_ast.ExtSlice object at 0x102ddef28>) (略)
変換処理で未サポートなどにより変換ができない部分は Warning が表示され、該当箇所以外の変換を続行します。
現在、Red Data Tool プロジェクトで行われている Chainer の Ruby移植版 Red Chainer の開発に Join し、ドッグフーディングも兼ねて私の作成した部分については py2rb.py を使いながら (py2rb.py の機能不足点も強化しながら)開発を進めています。
今後はRed Chainer の成長にあわせて py2rb.py の機能カバレッジや完成度も向上していければと考えています。
興味ある方は使ってみてください。