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
を使ってラッパーを生成します。