ErlPortなるものを発見
色々とネットサーフィンしてたらErlPortというErlangのライブラリを発見
なんとErlang環境からpythonとRubyを実行できる模様...凄すぎる
ErlangのモジュールはElixirから呼び出し可能なので勝ちました
前回の記事で作成したparser関数を(janomeを使った形態素解析)Elixirから呼び出すことを目標にErlPortを触ってみます
新規プロジェクトの立ち上げ
mix使って新規プロジェクトを立ち上げます
mixについてざっくり以下の記事で触れているのでよろしければご覧ください
さっそくプロジェクトを立ち上げます
> mix new call_python > cd call_python
ErlPortをインストールするようにmix.exsに記述します
./call_python/mix.exs
defp deps do [ {:erlport, "~> 0.9"} , # <-- ここに追加 # {:dep_from_hexpm, "~> 0.3.0"}, # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} ] end
以下のコマンドで外部ライブラリをダウンロードします
コンパイルに関しては自動でやってくれるのでスルーします
> mix deps.get
これでプロジェクトの準備は完了
.pyファイルの用意
今回はプロジェクトディレクトリの直下にpython_filesというディレクトリを置き
そこに.pyファイルを配置します
このパスがErlPortを使用する際に必要となります
で、ここに前回記述したparser関数を書いときます
ついでにチュートリアル的な引数を持たない最もシンプルなhello関数も作っときます
./call_python/python_files/janome_parser.py
from janome.tokenizer import Tokenizer def parser(value_str, tag=u"名詞"): #elixirから文字列を受け取る際にはutf-8へのdecodeが必要 decode_from_elixir = value_str.decode("utf-8") t = Tokenizer() res = t.tokenize(decode_from_elixir) if isinstance(tag, list): return [token.surface for token in res if token.part_of_speech.split(",")[0] in tag] else: return [token.surface for token in res if token.part_of_speech.split(",")[0] == tag] def hello(): print("hello world")
pythonファイルでの準備はこれでOKです
Elixir側から呼び出す
ではついにpythonで作成した関数を呼び出しましょう
公式ドキュメントを見るとこんな記述があります
1> {ok, PythonInstance1} = python:start().
{ok,<0.34.0>}
2> {ok, PythonInstance2} = python:start().
{ok,<0.36.0>}
3> python:stop(PythonInstance1)
ok
4> python:stop(PythonInstance2)
ok
python:start()というメゾットを呼び出すことでpython実行環境を持つプロセスを作成しているようです
これはErlangの記述なのでElixirの場合は
:python.start()
とします。またstart()にディレクトリのパスをリストで渡すことが可能です
バイナリで渡す必要があるので注意
他言語の癖でうっかり「"」を使うとエラーになります
:python.start([python_path: 'python_files'])
この作成したプロセスをpython.call()メゾットに渡すことで
.pyファイルに記述された関数をcallできる模様(公式より
2> python:call(P, operator, add, [2, 2]).
4
引数について
- 1st: 生成したpython実行環境のプロセス
- 2nd: 対象のファイル名
- 3rd: ファイル内部の関数名
- 4th: 与えたい引数(リストに格納)
先ほど作成したhello関数を対象に呼び出してみます
## プロセスを生成 iex(1)> {:ok, py_process} = :python.start([python_path: 'python_files']) {:ok, #PID<0.171.0>} ## 生成したプロセスを使用してpython実装のhello関数を呼び出す iex(2)> :python.call(py_process, :janome_parser, :hello, []) hello world #無事に出力された! :undefined #returnがないのでundefined
公式の通り、python.stop()でプロセスを終了させます
iex(3)> python.stop(py_process) ok
.pyの編集が反映されない時
.pyファイルに編集を加えた際にstartでパスを渡している場合にはプロセスを最生成する必要があるようです
編集分が反映されない時はもう一度、python.start()を呼び出します
.pyファイルを編集
def hello(): print("hello world") return 777
呼び出すも編集分が反映されていない
iex(4)> :python.call(py_process, :janome_parser, :hello, []) hello world :undefined
再度、プロセスを生成してhello()を呼び出すと反映されている
iex(6)> {:ok, py_process} = :python.start([python_path: 'python_files']) {:ok, #PID<0.180.0>} iex(7)> :python.call(py_process, :janome_parser, :hello, []) hello world 777
コマンドで叩いていたものを関数化しておきましょう
def call_hello_func_in_python do {:ok, py_process} = :python.start([python_path: 'python_files']) res = :python.call(py_process, :janome_parser, :hello, []) IO.puts("result: #{res}") :python.stop(py_process) :ok end
recompileをして上記の関数を呼び出してみます
iex(8)> recompile Compiling 1 file (.ex) iex(9)> CallPython.call_hello_func_in_python() hello world result: 777 :ok
良い感じですね
ここまで来たら呼び出す関数を別の物に変えるだけなので余裕
面倒なので最初から関数にしておきます
tagを渡す際に「u"名詞"」と記述しますがElixirからpythonにうまく渡せなかったので
python側でゴリ押ししました
def call_janome(value_str, tag) do {:ok, py_process} = :python.start([python_path: 'python_files']) result = :python.call(py_process, :janome_parser, :parser, [value_str, tag]) :python.stop(py_process) result end
./python_files/janome_parser.py
def parser(value_str, tag): decode_value_str_from_elixir = value_str.decode("utf-8") decode_tag_from_elixir = tag.decode("utf-8") #文字列をu-stringに変換する処理を追加 if encode_tag_from_elixir == "名詞": tag = u"名詞" t = Tokenizer() res = t.tokenize(encode_value_str_from_elixir) if isinstance(tag, list): return [token.surface for token in res if token.part_of_speech.split(",")[0] in tag] else: return [token.surface for token in res if token.part_of_speech.split(",")[0] == tag]
こいつを実行すると
iex(23)> CallPython.call_janome("何もない午後の入江を往く船をただ見つめていた", "名詞") [[20309], [21320, 24460], [20837, 27743], [33337]]
おや、戻り値が全てCharlistになってしまっている...
確かErlangにはStringがなくて全てbinaryになったはずなのでElixirでCharlistをStringに変換します
#resultをパイプ使ってStringに変換 result = :python.call(py_process, :janome_parser, :parser, [value_str, tag]) |> Enum.map(&(to_string(&1)))
さぁ実行してみる
iex(25)> recompile Compiling 1 file (.ex) :ok iex(26)> CallPython.call_janome("何もない午後の入江を往く船をただ見つめていた", "名詞") ["何", "午後", "入江", "船"]
うまくいきました!!
思ったように動作してくれるようになりました
Charlistの変換のところで割と迷った
まとめ
Elixirからpythonの関数呼ぶのすげー楽なんで流行りそう(流行れ
要はこれをElixirお得意の並列処理でゴリゴリすると...と考えると夢が広がりますね
長くなったので
という処理はまた次の記事に書きます(書くとはは言っていない
おまけのコーナー
pythonのやつの記述がゴリ押しすぎたので若干修正
ましになったかも
from janome.tokenizer import Tokenizer #ここに品詞をかいとけばu-stringに変換してくれる PART_OF_SPPECH = { "名詞": u"名詞", "形容詞": u"形容詞", } def parser(value_str, tag): def decoder(value_str, str_code="utf-8"): return value_str.decode(str_code) tag = PART_OF_SPPECH.get(decoder(tag)) t = Tokenizer() res = t.tokenize(decoder(value_str)) if isinstance(tag, list): return [token.surface for token in res if token.part_of_speech.split(",")[0] in tag] else: return [token.surface for token in res if token.part_of_speech.split(",")[0] == tag]