1 はじめに
株式会社Speeeで研究者をしている村田です。現在はRubyをデータサイエンスで使えるプログラミング言語にするための仕事に取り組んでいます。
これまでRubyはデータサイエンスの仕事では役に立たないプログラミング言語でした。仕事で実用に耐えられるツールが無く、ユーザが増えず、開発者も集まらない悪循環が原因です。こうした悪循環を解消するには、実用的な道具や環境を早急に整備して、データサイエンスの仕事でRubyを使う人を増やさなければなりません[1]。同時に、Rubyのための基盤ライブラリを開発し、応用ツールを開発しやすい土壌を作ることも重要です。
Rubyをデータサイエンスで使えるプログラミング言語にするのための環境整備には三つの道があると筆者は考えています。
一つ目の道は、既存ライブラリの改修や機能の拡張をしていく道です。2016年度のRuby Association開発助成プロジェクトで実施された「Rubyを用いた初等統計解析の整備と構築」は、この道に挑戦したプロジェクトでした。このプロジェクトの成果として、既存gemが持つ機能が足りないこと、さらに拡張や改善がやりにくい複雑な実装になってしまっていることにより、この道が非常に困難であることが分かりました。このプロジェクトの結果の詳細については最終報告書[2]をご覧ください。
二つ目の道は、Rubyのための道具を新しく作る道です。新しく作るといってもゼロから作りあげることだけではなく、Ruby向けのバインディングが提供されていないツールに対するバインディングの開発も含まれます。例えば、Tensorflow.rb[3]やRedDataTools[4、5]などのプロジェクトがこの道に該当します。
三つ目の道は、巨人の肩に乗る道です。PythonやRなどの、データサイエンス分野で実用されているプログラミング言語は、多数の実用的なツールを持っています。これらのツール群をRubyから使えるようにするのです。そうすることで、データサイエンス分野における既存資産とRubyで作られた既存資産の両者を連携させやすくなります。
本稿で述べるPyCall[6]は三つ目の道に相当する取り組みです。これは、Pythonというデータサイエンス界の巨人の肩に乗るための仕組みをRubyに提供し、さらにそれらのツールがRubyから使いやすくなるようなインターフェイス変換やヘルパーの提供も行います。
2 PyCallとは
2.1 Ruby-Pythonブリッジ
PyCallはCPythonインタープリタをRubyのライブラリにしてしまうことで、RubyからPythonの機能にアクセスするための機能を提供します。CPythonインタープリタの実体はlibpythonという共有ライブラリです。ですから、PyCallはlibpythonのRubyバインディングであると解釈できます。
しかしPyCallはただlibpythonが提供するAPIに対するインターフェイスを提供するだけではありません。PyCallはlibpythonが提供するAPIを利用し、RubyとPythonの間での相互運用性を高めるための独自の仕組みを構築しています。そのため、筆者はPyCallをバインディングではなくブリッジと呼んでいます。
このPyCallを使うことで、PythonのためのライブラリをあたかもRubyのライブラリであるかのようにRubyから利用できます。具体的な用例は本稿の後半で紹介します。
2.2 PyCallの基本機能
2.2.1 PythonオブジェクトをRubyから触る
 PyCallはPythonオブジェクトをRubyから操作するための仕組みを提供します。そのための代表的なクラスがPyCall::PyObjectです。
 PyCall::PyObjectクラスのオブジェクトは、属性アクセス、添え字アクセス、演算子、オブジェクト自身の呼び出しなど、Pythonオブジェクトが持つ基本機能に対するインターフェイスを持っています。これらの基本機能は、PyCall::PyObjectWrapperモジュールが供給します。
 PyCall::PyObjectWrapperモジュールによって供給されるPythonオブジェクトの基本機能は、インスタンスメソッド__pyobj__が存在し、これがPythonオブジェクトへのリファレンスを返すことを仮定して作られています。
 さらに、PyCall::PyObjectWrapperモジュールは、自身をincludeしたクラスにクラスメソッドwrap_classを供給します。wrap_classは、PythonのクラスオブジェクトをラップしたPyCall::PyObjectオブジェクトを引数にとり、そのクラスのインスタンスメソッドに対するラッパーメソッドを定義し、後述するクラス対応表に自分自身を登録します。
2.2.2 自動的な型変換
 PyCallは、PythonのクラスをRubyのどのクラスに対応づけるかを管理するクラス対応表を持っています。このクラス対応表にPythonのクラスオブジェクトとRubyのクラスオブジェクトのペアを登録しておくと、PythonオブジェクトをRubyインタープリタ側に持ってくる際に、対応するRubyのクラスのインスタンスを生成し、そのインスタンスでPythonオブジェクトをラップします。クラス対応表に登録されていない場合はPyCall::PyObjectクラスが使われます。
 このクラス対応表を用いない特殊ケースも存在します。それは、Pythonオブジェクトが整数、浮動小数点数、複素数、文字列のいずれかの場合です。これらの場合、Pythonオブジェクトをラップせず、RubyのInteger、Float、Complex、Stringに値を変換します。
PyCallはクラス対応表に次のペアをあらかじめ定義しています。
| Python | Ruby | 
|---|---|
| dict | PyCall::Dict | 
| list | PyCall::List | 
| set | PyCall::Set | 
| slice | PyCall::Slice | 
| tuple | PyCall::Tuple | 
| type | PyCall::TypeObject | 
2.2.3 PyCall.eval
 PyCall.eval関数は、文字列で与えられたPythonコードを評価します。
 PyCall.evalは省略可能なキーワード引数input_type: を持ちます。このキーワード引数を省略すると :evalが指定されたことになり、第1引数で指定するPythonコードが単独の式であると仮定し、その評価結果を返します。評価結果には、クラス対応表による自動型変換が適用されます。
 一方、このキーワード引数に :fileを指定すると、第1引数がPythonのスクリプトファイルなどから読み出された一つ以上の文の並びであると仮定されます。クラス定義のような文を評価したい場合は、キーワード引数input_type: に :fileを指定する必要があります。
2.2.4 pyimportとpyfrom
 Pythonでは、import numpy as npと書くと、numpyモジュールにnpという名前でアクセスできるようになります。また、from PIL import Imageと書くことで、PILモジュール内のImageのみをインポートできます。
 PyCall::Importモジュールが提供するpyimportメソッドを使うと、このimport ... as ...と同様にPythonのモジュールをRubyのモジュール内にインポートできます。キーワード引数as: に名前を渡せばインポートしたモジュールにアクセスするための名前を指定できます。次の例はPythonのnumpyモジュールをRubyのPyモジュールの特異メソッドnpでアクセスできるようにします。
require 'pycall/import' module Py extend PyCall::Import pyimport :numpy, as: :np end
 また、同じくPyCall::Importモジュールが提供するpyfromメソッドを使うことで、from ... import ...と同様にPythonのモジュール内のオブジェクトをRubyのモジュール内にインポートできます。次の例は、PILモジュール内のImageクラスをPyモジュール内にインポートします。
require 'pycall/import' module Py extend PyCall::Import pyfrom :PIL, import: :Image end
 PyCall::Importはrequire "pycall"ではロードされません。これを利用するにはrequire "pycall/import"する必要があります。
2.2.5 PyCall.wrap_ruby_callable
 PyCall.wrap_ruby_callableは、Rubyオブジェクトに対するラッパーとなるPythonオブジェクトを作ります。このメソッドで作ったラッパーはcallableとなり、Pythonインタープリタからこのラッパーオブジェクトを呼び出すと、元のRubyオブジェクトのcallメソッドが呼び出されます。
 PyCallは、Python側に渡すRubyオブジェクトがProcオブジェクトである場合、自動的にPyCall.wrap_ruby_callableを使ってラッパーを生成します。
