前回のはなし
Elixirからpythonの関数を呼び出して、最終的にjanomeを使って形態素解析を行いました
本当はpythonのclassメゾットの呼び出しとElixirに対して
Atomのデータを返すということがやりたかったんですけど
そこそこ記事が長くなってしまったのでカット
今回の記事で触っていく
ErlPortの公式にclassメゾットの呼び出しについてほとんど記述が無かったので割としんどかった
python側での準備
単純なclassメゾットを用意
class EasyClassMethod: def output_msg(self, msg): decode_msg = msg.decode("utf-8") print(decode_msg) return True
classなのにただ文字列を出力するだけのカス
文字列のdecodeについては前回の記事で触れてるのでどうぞ
Elixir側での準備
ほとんど前の記事のものと同じで異なるのは:python.callを使用して
クラスのインスタンスをさりげなく作っているところです
demodule CallPython do def call_esay_method(msg) do {:ok, py_processs} = :python.start([python_path: 'python_files']) #EasyClassMethodのインスタンスを作ってる(object) obj = :python.call(py_processs, :test_call, :EasyClassMethod, []) #呼び出す関数はアトムの文字列で与える必要あり,引数の第1にobjectを渡す res = :python.call(py_processs, :test_call, :"EasyClassMethod.output_msg", [obj, msg]) IO.puts(res) :python.stop(py_processs) :ok end end
とりえあず動作を見てみると
iex(3)> CallPython.call_esay_method("hello world") hello world true :ok
いい感じですね
それぞれの出力値を追っていくと
obj
謎のバイナリーデータが入っている
#objのreturn {:"$erlport.opaque", :python, <<128, 2, 99, 116, 101, 115, 116, 95, 99, 97, 108, 108, 10, 69, 97, 115, 121, 67, 108, 97, 115, 115, 77, 101, 116, 104, 111, 100, 10, 113, 0, 41, 129, 113, 1, 46>>}
res
メゾットの戻り値が来ている。いいね
true
クラス化したJanomeParserを呼び出す
とりあえず基礎の部分は抑えたのでこの記事で作成したclassメゾットを呼び出してみる
python
class JanomeParser: def __init__(self): print("come here: init method") self.t = Tokenizer() def parser(self, value_str, tag=u"名詞"): res = self.t.tokenize(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]
Elixir
def class_caller(value_str, tag) do {:ok, py_process} = :python.start([python_path: 'python_files']) obj = :python.call(py_process, :test_call, :JanomeParser, []) res = :python.call(py_process, :test_call, :"JanomeParser.parser", [obj, value_str, tag]) |> Enum.map(&(to_string(&1))) :python.stop(py_process) res end
今まで通り呼び出すが、実は2点問題がある
とりあえず実行結果を先にお見せします
iex(3)> CallPython.class_caller("言葉一つで足りないくらい全部壊れてしまうような", "名詞") come here: init method ** (ErlangError) Erlang error: {:python, :"builtins.ValueError", 'unsupported data type: <class \'test_call.JanomeParser\'>', : : (call_python) lib/call_python.ex:46: CallPython.class_caller/2
どうやらinitiを呼び出しまでは問題なく行われているが
self.t = Tokenizer()
ってのが上手く行かないっぽい
あとparserに渡した文字列を毎度のごとくdecodeする必要がある
ということでdecodeをしつつ、initでinstanceを生成するのを止めたら動いた
class JanomeParser(object): def __init__(self): print("come here") def parser(self, 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]
iex(5)> CallPython.class_caller("言葉一つで足りないくらい全部壊れてしまうような", "名詞") come here ["言葉", "一つ", "全部", "よう"]
いい感じだけど、class化した意味があんまりないのがつらい
外部でインスタンス作ってinitに渡してあげればいいかもなと思い試したけどダメでした(デデドン
class JanomeParser(object): def __init__(self, tokenizer): print("come here") self.t = tokenizer def parser(self, value_str, tag): def decoder(value_str, str_code="utf-8"): return value_str.decode(str_code) tag = PART_OF_SPPECH.get(decoder(tag)) res = self.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] def create_tokenizer_instance(): return Tokenizer()
def class_caller(value_str, tag) do {:ok, py_process} = :python.start([python_path: 'python_files']) tokenizer = :python.call(py_process, :test_call, :create_tokenizer_instance, []) obj = :python.call(py_process, :test_call, :JanomeParser, [tokenizer]) res = :python.call(py_process, :test_call, :"JanomeParser.parser", [obj, value_str, tag]) |> Enum.map(&(to_string(&1))) :python.stop(py_process) res end
実行結果
iex(8)> CallPython.class_caller("言葉一つで足りないくらい全部壊れてしまうような", "名詞") come here: init method ** (ErlangError) Erlang error: {:python, :"builtins.ValueError", 'unsupported data type: <class \'test_call.JanomeParser\'>', : : (call_python) lib/call_python.ex:46: CallPython.class_caller/2
さっきと同じエラーが。要研究ですね
pythonからElixirにAtomを返す
python側の関数から
:ok {:ok, value} {:error, reason}
のようなAtomを返すようなことがしたい場合があるかもしれない
実際にElixirでパターンマッチする際にはAtomの:okやら:errorをよくマッチさせるので使い道は多そう
Elixir側でやることはほとんどなくpython側でのごちゃごちゃが必要になる
公式ドキュメントをみると細々と記述されていた
まずは「erlport」をpython側でインストールする
pip install erlport pip3 install erlport
from erlport.erlterms import Atom def return_atom_to_elixir(): #Atomメゾットには文字列を与える return Atom("ok")
あとはこいつをElixirから呼び出すだけなのでいつも通りですね
Elixirとpythonの型の対応は公式ドキュメントの下の方に記述されています
ほとんどの型はそのまま受け渡しできますが
- List
- Atom
に関してはこのように関数を使って定義する必要があります
他にもpython側のNoneはElixir側ではundefinedになるなど多少異なる点があるので注意
いまさらですがErlangでは文字列はなく全てbinaryとして扱われるのでpython側でdecode, encodeが必要です
感想
classメゾットの呼び出しで結構つまづいた
もっとスッキリ使えればと思う一方、ErlPortの仕組みが気になる
そういえばErlang作者の1人であるJoe Armstrong's さんが亡くなったそうです
ElixirがあるのはErlangのおかげ。ErlangがあるのはJoe Armstrong'sのおかげです
残念です。少しでも多くの人にElixirを触ってもらえるようにアウトプット頑張ります