はじめに
Pythonx は Elixir から Python コードを実行するモジュールです
リポジトリーの注意書きにある通り、まだ開発中です
Pythonx is still very much a work in progress and is mainly intended for some proof of concept at current stage.
Pythonx はまだ開発中であり、現段階では主に概念実証を目的としています。
本記事では Elixir と Python を行ったり来たりして遊びます
リポジトリーの README に書いてある Low-Level から Very High Level まで各段階のコードを Livebook 上で 実行してみました
実装したノートブックはこちら
Low-Level
セットアップ
セットアップセルで Pythonx と Kino をインストールします
Mix.install([
{:pythonx, "~> 0.2"},
{:kino, "~> 0.14"}
])
初期処理
各モジュールに alias を付けておきます
alias Pythonx.C
alias Pythonx.C.PyDict
alias Pythonx.C.PyErr
alias Pythonx.C.PyFloat
alias Pythonx.C.PyList
alias Pythonx.C.PyLong
alias Pythonx.C.PyObject
alias Pythonx.C.PyRun
alias Pythonx.C.PyTuple
alias Pythonx.C.PyUnicode
Python 実行環境を初期化します
内部的には NIF をロードしています
Pythonx.initialize_once()
Python コード内で使うグローバル変数、ローカル変数を辞書型で用意します
globals = PyDict.new()
locals = PyDict.new()
ローカル変数
Long 型の変数 a をローカル変数に追加します
a = PyLong.from_long(1)
PyDict.set_item_string(locals, "a", a)
以下のようにするとローカル変数から値を取り出せます
PyDict.get_item_string(locals, "a") |> PyLong.as_long()
実行結果
1
以下のようにして標準出力に出すことも可能です
PyObject.print(a, :stdout, 0)
標準出力
1
locals を出力すると、 Python の辞書型であることが分かります
PyObject.print(locals, :stdout, 0)
標準出力
{'a': 1}
キーとして存在しないものを取り出そうとすると nil が返ってきます
PyDict.get_item_string(locals, "l")
実行結果
nil
ローカル変数に b を追加します
b = PyLong.from_long(2)
PyDict.set_item_string(locals, "b", b)
PyObject.print(locals, :stdout, 0)
標準出力
{'a': 1, 'b': 2}
以下のコードで locals を Elixir の Map に変換できます
items = PyDict.items(locals)
Enum.into(0..(PyList.size(items) - 1), %{}, fn index ->
items
|> PyList.get_item(index)
|> then(fn tuple ->
{
tuple |> PyTuple.get_item(0) |> PyUnicode.as_utf8(),
tuple |> PyTuple.get_item(1) |> PyLong.as_long()
}
end)
end)
実行結果
%{"a" => 1, "b" => 2}
Python コードの実行
PyRun.string
で Python コードを実行できます
PyRun.string("c = a + b", C.py_file_input(), globals, locals)
PyObject.print(locals, :stdout, 0)
標準出力
{'a': 1, 'b': 2, 'c': 3}
c
の値を取り出すと、確かに 3 になっています
PyDict.get_item_string(locals, "c") |> PyLong.as_long()
実行結果
3
エラーが発生した場合はエラーオブジェクトが返ってきて、エラーメッセージも参照できます
result = PyRun.string("n = m + 1", C.py_file_input(), globals, locals)
case result do
%PyErr{} ->
PyUnicode.as_utf8(result.value)
_ ->
nil
end
条件式や内包表記などもしっかり実行可能です
PyRun.string("d = a / b", C.py_file_input(), globals, locals)
PyRun.string("e = 99 if a == 0 else -1", C.py_file_input(), globals, locals)
PyRun.string("f = [i ** 2 for i in range(10)]", C.py_file_input(), globals, locals)
PyObject.print(locals, :stdout, 0)
標準出力
{'a': 1, 'b': 2, 'c': 3, 'd': 0.5, 'e': -1, 'f': [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]}
小数を取り出す場合は PyFloat.as_double
を使います
PyDict.get_item_string(locals, "d") |> PyFloat.as_double()
実行結果
0.5
グローバル変数
グローバル変数とローカル変数の扱いの違いを確認してみましょう
グローバル変数とローカル変数の両方に x
を定義し、グローバル変数だけに z
を定義します
globals = PyDict.new()
locals = PyDict.new()
PyDict.set_item_string(locals, "x", PyLong.from_long(1))
PyDict.set_item_string(globals, "x", PyLong.from_long(1))
PyDict.set_item_string(globals, "z", PyLong.from_long(99))
PyObject.print(locals, :stdout, 0)
PyObject.print(globals, :stdout, 0)
標準出力
{'x': 1}
{'x': 1, 'z': 99}
両方に存在している x
を更新すると、ローカル変数の x
だけが更新されます
グローバル変数だけに存在している z
を参照して z
を更新すると、ローカル変数に新しく z
が定義されます
z
を参照して新しい変数 y
を定義すると、ローカル変数の z
を参照します
PyRun.string("""
x = 2
z = z + 1
y = z + 1
""", C.py_file_input(), globals, locals)
PyObject.print(locals, :stdout, 0)
PyObject.print(globals, :stdout, 0)
標準出力
{'x': 2, 'z': 100, 'y': 101}
{'x': 1, 'z': 99, '__builtins__': {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': ModuleSpec(name='builtins', loader=<class '_frozen_importlib.BuiltinImporter'>), '__build_class__': <built-in function __build_class__>, '__import__': <built-in function __import__>, 'abs': <built-in function abs>, 'all': <built-in function all>, 'any': <built-in function any>, 'ascii': <built-in function ascii>, 'bin': <built-in function bin>, 'breakpoint': <built-in function breakpoint>, 'callable': <built-in function callable>, 'chr': <built-in function chr>, 'compile': <built-in function compile>, 'delattr': <built-in function delattr>, 'dir': <built-in function dir>, 'divmod': <built-in function divmod>, 'eval': <built-in function eval>, 'exec': <built-in function exec>, 'format': <built-in function format>, 'getattr': <built-in function getattr>, 'globals': <built-in function globals>, 'hasattr': <built-in function hasattr>, 'hash': <built-in function hash>, 'hex': <built-in function hex>, 'id': <built-in function id>, 'input': <built-in function input>, 'isinstance': <built-in function isinstance>, 'issubclass': <built-in function issubclass>, 'iter': <built-in function iter>, 'len': <built-in function len>, 'locals': <built-in function locals>, 'max': <built-in function max>, 'min': <built-in function min>, 'next': <built-in function next>, 'oct': <built-in function oct>, 'ord': <built-in function ord>, 'pow': <built-in function pow>, 'print': <built-in function print>, 'repr': <built-in function repr>, 'round': <built-in function round>, 'setattr': <built-in function setattr>, 'sorted': <built-in function sorted>, 'sum': <built-in function sum>, 'vars': <built-in function vars>, 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': <class 'bool'>, 'memoryview': <class 'memoryview'>, 'bytearray': <class 'bytearray'>, 'bytes': <class 'bytes'>, 'classmethod': <class 'classmethod'>, 'complex': <class 'complex'>, 'dict': <class 'dict'>, 'enumerate': <class 'enumerate'>, 'filter': <class 'filter'>, 'float': <class 'float'>, 'frozenset': <class 'frozenset'>, 'property': <class 'property'>, 'int': <class 'int'>, 'list': <class 'list'>, 'map': <class 'map'>, 'object': <class 'object'>, 'range': <class 'range'>, 'reversed': <class 'reversed'>, 'set': <class 'set'>, 'slice': <class 'slice'>, 'staticmethod': <class 'staticmethod'>, 'str': <class 'str'>, 'super': <class 'super'>, 'tuple': <class 'tuple'>, 'type': <class 'type'>, 'zip': <class 'zip'>, '__debug__': True, 'BaseException': <class 'BaseException'>, 'Exception': <class 'Exception'>, 'TypeError': <class 'TypeError'>, 'StopAsyncIteration': <class 'StopAsyncIteration'>, 'StopIteration': <class 'StopIteration'>, 'GeneratorExit': <class 'GeneratorExit'>, 'SystemExit': <class 'SystemExit'>, 'KeyboardInterrupt': <class 'KeyboardInterrupt'>, 'ImportError': <class 'ImportError'>, 'ModuleNotFoundError': <class 'ModuleNotFoundError'>, 'OSError': <class 'OSError'>, 'EnvironmentError': <class 'OSError'>, 'IOError': <class 'OSError'>, 'EOFError': <class 'EOFError'>, 'RuntimeError': <class 'RuntimeError'>, 'RecursionError': <class 'RecursionError'>, 'NotImplementedError': <class 'NotImplementedError'>, 'NameError': <class 'NameError'>, 'UnboundLocalError': <class 'UnboundLocalError'>, 'AttributeError': <class 'AttributeError'>, 'SyntaxError': <class 'SyntaxError'>, 'IndentationError': <class 'IndentationError'>, 'TabError': <class 'TabError'>, 'LookupError': <class 'LookupError'>, 'IndexError': <class 'IndexError'>, 'KeyError': <class 'KeyError'>, 'ValueError': <class 'ValueError'>, 'UnicodeError': <class 'UnicodeError'>, 'UnicodeEncodeError': <class 'UnicodeEncodeError'>, 'UnicodeDecodeError': <class 'UnicodeDecodeError'>, 'UnicodeTranslateError': <class 'UnicodeTranslateError'>, 'AssertionError': <class 'AssertionError'>, 'ArithmeticError': <class 'ArithmeticError'>, 'FloatingPointError': <class 'FloatingPointError'>, 'OverflowError': <class 'OverflowError'>, 'ZeroDivisionError': <class 'ZeroDivisionError'>, 'SystemError': <class 'SystemError'>, 'ReferenceError': <class 'ReferenceError'>, 'MemoryError': <class 'MemoryError'>, 'BufferError': <class 'BufferError'>, 'Warning': <class 'Warning'>, 'UserWarning': <class 'UserWarning'>, 'DeprecationWarning': <class 'DeprecationWarning'>, 'PendingDeprecationWarning': <class 'PendingDeprecationWarning'>, 'SyntaxWarning': <class 'SyntaxWarning'>, 'RuntimeWarning': <class 'RuntimeWarning'>, 'FutureWarning': <class 'FutureWarning'>, 'ImportWarning': <class 'ImportWarning'>, 'UnicodeWarning': <class 'UnicodeWarning'>, 'BytesWarning': <class 'BytesWarning'>, 'ResourceWarning': <class 'ResourceWarning'>, 'ConnectionError': <class 'ConnectionError'>, 'BlockingIOError': <class 'BlockingIOError'>, 'BrokenPipeError': <class 'BrokenPipeError'>, 'ChildProcessError': <class 'ChildProcessError'>, 'ConnectionAbortedError': <class 'ConnectionAbortedError'>, 'ConnectionRefusedError': <class 'ConnectionRefusedError'>, 'ConnectionResetError': <class 'ConnectionResetError'>, 'FileExistsError': <class 'FileExistsError'>, 'FileNotFoundError': <class 'FileNotFoundError'>, 'IsADirectoryError': <class 'IsADirectoryError'>, 'NotADirectoryError': <class 'NotADirectoryError'>, 'InterruptedError': <class 'InterruptedError'>, 'PermissionError': <class 'PermissionError'>, 'ProcessLookupError': <class 'ProcessLookupError'>, 'TimeoutError': <class 'TimeoutError'>, 'open': <built-in function open>, 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2022 Python Software Foundation.
All Rights Reserved.
Copyright (c) 2000 BeOpen.com.
All Rights Reserved.
Copyright (c) 1995-2001 Corporation for National Research Initiatives.
All Rights Reserved.
Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam.
All Rights Reserved., 'credits': Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands
for supporting Python development. See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.}}
グローバル変数にはビルトインモジュールが追加されていました
BEAM Level
セットアップ
セットアップセルで Pythonx と Kino をインストールします
Mix.install([
{:pythonx, "~> 0.2"},
{:kino, "~> 0.14"}
])
初期処理
BEAM 用モジュールに alias を付けておきます
alias Pythonx.Beam
NIF をロードします
Pythonx.initialize_once()
グローバル変数を定義します
BEAM レベルの場合、 Beam.encode
と Beam.decode
で Python と Elixir を行き来できるようになっています
Low-Level の標準出力も可能です
globals = Beam.encode(%{x: 1})
Pythonx.C.PyObject.print(globals.ref, :stdout, 0)
Beam.decode(globals)
標準出力
{'x': 1}
実行結果
%{"x" => 1}
同様にローカル変数も定義します
locals = Beam.encode(%{a: 1, b: 2})
Pythonx.C.PyObject.print(locals.ref, :stdout, 0)
Beam.decode(locals)
標準出力
{'b': 2, 'a': 1}
実行結果
%{"a" => 1, "b" => 2}
Python コードの実行
Beam.PyRun
で Python コードを実行できます
Beam.PyRun.string("c = a + b", Beam.py_file_input(), globals, locals)
Beam.PyRun.string("l = [i ** 2 for i in range(10)]", Beam.py_file_input(), globals, locals)
Beam.PyRun.string("""
m = {"x": 99, "y": 100}
m["x"] = m["x"] + 2
""", Beam.py_file_input(), globals, locals)
Pythonx.C.PyObject.print(locals.ref, :stdout, 0)
Beam.decode(locals)
標準出力
{'b': 2, 'a': 1, 'c': 3, 'l': [0, 1, 4, 9, 16, 25, 36, 49, 64, 81], 'm': {'x': 101, 'y': 100}}
実行結果
%{
"a" => 1,
"b" => 2,
"c" => 3,
"l" => [0, 1, 4, 9, 16, 25, 36, 49, 64, 81],
"m" => %{"x" => 101, "y" => 100}
}
Python コード実行をパイプで繋げる関数を定義してみましょう
py_beam_pipe = fn locals, code ->
beam_locals = Beam.encode(locals)
Beam.PyRun.string(code, Beam.py_file_input(), Beam.encode(%{}), beam_locals)
Beam.decode(beam_locals)
end
Python と Elixir の世界をパイプで行ったり来たりできます
%{x: ["a", "b", "c"]}
|> py_beam_pipe.("""
y = {}
for i, v in enumerate(x):
y[v] = i
""")
|> then(fn locals ->
Map.put(locals, "elixir", "hello")
end)
|> py_beam_pipe.("""
if len(x) == 3:
x.sort(reverse=True)
""")
|> then(fn locals ->
locals
|> Map.put("y", Map.put(locals["y"], "x", 99))
|> Map.put("x", ["z" | locals["x"]])
end)
|> py_beam_pipe.("""
python = "hello"
""")
実行結果
%{
"elixir" => "hello",
"i" => 2,
"python" => "hello",
"v" => "c",
"x" => ["z", "c", "b", "a"],
"y" => %{"a" => 0, "b" => 1, "c" => 2, "x" => 99}
}
High-Level
セットアップ
セットアップセルで Pythonx と Kino をインストールします
Mix.install([
{:pythonx, "~> 0.2"},
{:kino, "~> 0.14"}
])
初期処理
使用するモジュールに alias を付けます
alias Pythonx.State
alias Pythonx.PyRun
NIF をロードします
Pythonx.initialize_once()
グローバル変数とローカル変数を State 内に定義します
state = State.new(globals: %{x: 1}, locals: %{a: 1, b: 2})
Python コードの実行
PyRun.string
で Python コードを実行できます
{result, state} = PyRun.string("c = a + b", Pythonx.py_file_input(), state)
実行結果
{#PyObject<
type: "NoneType",
repr: "None"
>, %Pythonx.State{globals: #PyObject<
type: "dict",
repr: "{'x': 1, '__builtins__': {'__name__': 'builtins', '__doc__': \"Built-in functions, exceptions, and other objects.\\n\\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.\", '__package__': '', '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': ModuleSpec(name='builtins', loader=<class '_frozen_importlib.BuiltinImporter'>), '__build_class__': <built-in function __build_class__>, '__import__': <built-in function __import__>, 'abs': <built-in function abs>, 'all': <built-in function all>, 'any': <built-in function any>, 'ascii': <built-in function ascii>, 'bin': <built-in function bin>, 'breakpoint': <built-in function breakpoint>, 'callable': <built-in function callable>, 'chr': <built-in function chr>, 'compile': <built-in function compile>, 'delattr': <built-in function delattr>, 'dir': <built-in function dir>, 'divmod': <built-in function divmod>, 'eval': <built-in function eval>, 'exec': <built-in function exec>, 'format': <built-in function format>, 'getattr': <built-in function getattr>, 'globals': <built-in function globals>, 'hasattr': <built-in function hasattr>, 'hash': <built-in function hash>, 'hex': <built-in function hex>, 'id': <built-in function id>, 'input': <built-in function input>, 'isinstance': <built-in function isinstance>, 'issubclass': <built-in function issubclass>, 'iter': <built-in function iter>, 'len': <built-in function len>, 'locals': <built-in function locals>, 'max': <built-in function max>, 'min': <built-in function min>, 'next': <built-in function next>, 'oct': <built-in function oct>, 'ord': <built-in function ord>, 'pow': <built-in function pow>, 'print': <built-in function print>, 'repr': <built-in function repr>, 'round': <built-in function round>, 'setattr': <built-in function setattr>, 'sorted': <built-in function sorted>, 'sum': <built-in function sum>, 'vars': <built-in function vars>, 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': <class 'bool'>, 'memoryview': <class 'memoryview'>, 'bytearray': <class 'bytearray'>, 'bytes': <class 'bytes'>, 'classmethod': <class 'classmethod'>, 'complex': <class 'complex'>, 'dict': <class 'dict'>, 'enumerate': <class 'enumerate'>, 'filter': <class 'filter'>, 'float': <class 'float'>, 'frozenset': <class 'frozenset'>, 'property': <class 'property'>, 'int': <class 'int'>, 'list': <class 'list'>, 'map': <class 'map'>, 'object': <class 'object'>, 'range': <class 'range'>, 'reversed': <class 'reversed'>, 'set': <class 'set'>, 'slice': <class 'slice'>, 'staticmethod': <class 'staticmethod'>, 'str': <class 'str'>, 'super': <class 'super'>, 'tuple': <class 'tuple'>, 'type': <class 'type'>, 'zip': <class 'zip'>, '__debug__': True, 'BaseException': <class 'BaseException'>, 'Exception': <class 'Exception'>, 'TypeError': <class 'TypeError'>, 'StopAsyncIteration': <class 'StopAsyncIteration'>, 'StopIteration': <class 'StopIteration'>, 'GeneratorExit': <class 'GeneratorExit'>, 'SystemExit': <class 'SystemExit'>, 'KeyboardInterrupt': <class 'KeyboardInterrupt'>, 'ImportError': <class 'ImportError'>, 'ModuleNotFoundError': <class 'ModuleNotFoundError'>, 'OSError': <class 'OSError'>, 'EnvironmentError': <class 'OSError'>, 'IOError': <class 'OSError'>, 'EOFError': <class 'EOFError'>, 'RuntimeError': <class 'RuntimeError'>, 'RecursionError': <class 'RecursionError'>, 'NotImplementedError': <class 'NotImplementedError'>, 'NameError': <class 'NameError'>, 'UnboundLocalError': <class 'UnboundLocalError'>, 'AttributeError': <class 'AttributeError'>, 'SyntaxError': <class 'SyntaxError'>, 'IndentationError': <class 'IndentationError'>, 'TabError': <class 'TabError'>, 'LookupError': <class 'LookupError'>, 'IndexError': <class 'IndexError'>, 'KeyError': <class 'KeyError'>, 'ValueError': <class 'ValueError'>, 'UnicodeError': <class 'UnicodeError'>, 'UnicodeEncodeError': <class 'UnicodeEncodeError'>, 'UnicodeDecodeError': <class 'UnicodeDecodeError'>, 'UnicodeTranslateError': <class 'UnicodeTranslateError'>, 'AssertionError': <class 'AssertionError'>, 'ArithmeticError': <clas" <> ...
>, locals: %{"a" => 1, "b" => 2, "c" => 3}}}
グローバル変数にはビルトインモジュールが追加されています
state.locals
でローカル変数をそのまま Elixir の Map として取得できます
state.locals
実行結果
%{"a" => 1, "b" => 2, "c" => 3}
High-Level でも同じようにパイプで繋いでみましょう
py_run_pipe = fn locals, code ->
state = State.new(locals: locals)
{_, state} = PyRun.string(code, Pythonx.py_file_input(), state)
state.locals
end
%{x: ["a", "b", "c"]}
|> py_run_pipe.("""
y = {}
for i, v in enumerate(x):
y[v] = i
""")
|> then(fn locals ->
Map.put(locals, "elixir", "hello")
end)
|> py_run_pipe.("""
if len(x) == 3:
x.sort(reverse=True)
""")
|> then(fn locals ->
locals
|> Map.put("y", Map.put(locals["y"], "x", 99))
|> Map.put("x", ["z" | locals["x"]])
end)
|> py_run_pipe.("""
python = "hello"
""")
実行結果
%{
"elixir" => "hello",
"i" => 2,
"python" => "hello",
"v" => "c",
"x" => ["z", "c", "b", "a"],
"y" => %{"a" => 0, "b" => 1, "c" => 2, "x" => 99}
}
Very High Level
セットアップ
セットアップセルで Pythonx と Kino をインストールします
Mix.install([
{:pythonx, "~> 0.2"},
{:kino, "~> 0.14"}
])
初期処理
関数を使いやすくするため、 import
しておきます
import Pythonx
NIF をロードします
Pythonx.initialize_once()
Python コードの実行
pyeval
を使うと Python コードを実行し、その結果を return
で指定した Elixir 変数に返すことができます
pyeval("""
import sys
platform = sys.platform
l = [i ** 2 for i in range(3)]
m = {'x': l[0], 'y': l[1]}
""", return: [:platform, :l, :m])
{platform, l, m}
実行結果
{"linux", [0, 1, 4], %{"x" => 0, "y" => 1}}
更に pyinline
を使用すると、 Elixir で定義していた変数をそのまま Python 上で参照できます
x = 2
y = -1
pyinline("""
z = []
for i in [1, 2, 3, 4, 5]:
if i % x == 0:
z.append(y)
else:
z.append(i)
w = {'x': x, 'y': y}
""", return: [:z, :w])
{z, w}
実行結果
{[1, -1, 3, -1, 5], %{"x" => 2, "y" => -1}}
Python スクリプトの実行
Python スクリプトをファイルとして保存します
File.write!("/tmp/sample_script.py", """
def main():
print("Hello, Python!")
if __name__ == "__main__":
main()
""")
以下のコードで Python スクリプトを実行できます
python3! "/tmp/sample_script.py", into: []
ただし、私の実行環境(Livebook 0.14.0 の Docker コンテナ)ではエラーが発生し、実行結果が以下のようになります
{[], 127}
現状の Pythonx は Python 3.8.6 用にビルドされており、コンテナ上には Python 3.10 しかインストールされていないためです
以下のコードを実行すると、無理矢理コンテナ上にインストール済の Python で実行させることができます
System.cmd("ln", ["-sfn", "/usr/bin/python3", Pythonx.python3_executable()])
その場合、 env: [{"PYTHONHOME", "/usr/"}]
で環境変数 PYTHONHOME
を指定する必要があります
python3! "/tmp/sample_script.py", env: [{"PYTHONHOME", "/usr/"}], into: []
実行結果
{["Hello, Python!\n"], 0}
pip
で外部モジュールをインストールしたり、外部モジュールを読み込んで Python スクリプトを実行したりすることもできるのですが、私のコンテナ環境や M2 Mac のローカル環境ではうまくいきませんでした
まとめ
Python と Elixir を織り交ぜて実行できるのは非常に面白い体験でした
特に pyinline
で変数が相互に行き来できるのは面白いですね
今後の開発に期待です