3
2

Elixir Livebook で Python コードを実行する【Pythonx】

Last updated at Posted at 2024-09-27

はじめに

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

セットアップ

セットアップセルで PythonxKino をインストールします

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

セットアップ

セットアップセルで PythonxKino をインストールします

Mix.install([
  {:pythonx, "~> 0.2"},
  {:kino, "~> 0.14"}
])

初期処理

BEAM 用モジュールに alias を付けておきます

alias Pythonx.Beam

NIF をロードします

Pythonx.initialize_once()

グローバル変数を定義します
BEAM レベルの場合、 Beam.encodeBeam.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

セットアップ

セットアップセルで PythonxKino をインストールします

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

セットアップ

セットアップセルで PythonxKino をインストールします

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 で変数が相互に行き来できるのは面白いですね

今後の開発に期待です

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2