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

## はじめに

Pythonx は Elixir から Python コードを実行するモジュールです

https://github.com/cocoa-xu/pythonx

リポジトリーの注意書きにある通り、まだ開発中です

> 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 上で 実行してみました

実装したノートブックはこちら

https://github.com/RyoWakabayashi/elixir-learning/tree/main/livebooks/pythonx

## Low-Level

### セットアップ

セットアップセルで [Pythonx](https://github.com/cocoa-xu/pythonx) と [Kino](https://github.com/livebook-dev/kino) をインストールします

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

### 初期処理

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

```elixir
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](https://www.erlang.org/doc/system/nif.html) をロードしています

```elixir
Pythonx.initialize_once()
```

Python コード内で使うグローバル変数、ローカル変数を辞書型で用意します

```elixir
globals = PyDict.new()
locals = PyDict.new()
```

### ローカル変数

Long 型の変数 a をローカル変数に追加します

```elixir
a = PyLong.from_long(1)
PyDict.set_item_string(locals, "a", a)
```

以下のようにするとローカル変数から値を取り出せます

```elixir
PyDict.get_item_string(locals, "a") |> PyLong.as_long()
```

実行結果

```elixir
1
```

以下のようにして標準出力に出すことも可能です

```elixir
PyObject.print(a, :stdout, 0)
```

標準出力

```python
1
```

locals を出力すると、 Python の辞書型であることが分かります

```elixir
PyObject.print(locals, :stdout, 0)
```

標準出力

```python
{'a': 1}
```

キーとして存在しないものを取り出そうとすると nil が返ってきます

```elixir
PyDict.get_item_string(locals, "l")
```

実行結果

```elixir
nil
```

ローカル変数に b を追加します

```elixir
b = PyLong.from_long(2)
PyDict.set_item_string(locals, "b", b)

PyObject.print(locals, :stdout, 0)
```

標準出力

```python
{'a': 1, 'b': 2}
```

以下のコードで locals を Elixir の Map に変換できます

```elixir
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)
```

実行結果

```elixir
%{"a" => 1, "b" => 2}
```

### Python コードの実行

`PyRun.string` で Python コードを実行できます

```elixir
PyRun.string("c = a + b", C.py_file_input(), globals, locals)

PyObject.print(locals, :stdout, 0)
```

標準出力

```python
{'a': 1, 'b': 2, 'c': 3}
```

`c` の値を取り出すと、確かに 3 になっています

```elixir
PyDict.get_item_string(locals, "c") |> PyLong.as_long()
```

実行結果

```elixir
3
```

エラーが発生した場合はエラーオブジェクトが返ってきて、エラーメッセージも参照できます

```elixir
result = PyRun.string("n = m + 1", C.py_file_input(), globals, locals)

case result do
  %PyErr{} ->
    PyUnicode.as_utf8(result.value)
  _ ->
    nil
end
```

条件式や内包表記などもしっかり実行可能です

```elixir
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)
```

標準出力

```python
{'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` を使います

```elixir
PyDict.get_item_string(locals, "d") |> PyFloat.as_double()
```

実行結果

```elixir
0.5
```

### グローバル変数

グローバル変数とローカル変数の扱いの違いを確認してみましょう

グローバル変数とローカル変数の両方に `x` を定義し、グローバル変数だけに `z` を定義します

```elixir
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)
```

標準出力

```python
{'x': 1}
{'x': 1, 'z': 99}
```

両方に存在している `x` を更新すると、ローカル変数の `x` だけが更新されます
グローバル変数だけに存在している `z` を参照して `z` を更新すると、ローカル変数に新しく `z` が定義されます
`z` を参照して新しい変数 `y` を定義すると、ローカル変数の `z` を参照します

```elixir
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)
```

標準出力

```python
{'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](https://github.com/cocoa-xu/pythonx) と [Kino](https://github.com/livebook-dev/kino) をインストールします

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

### 初期処理

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

```elixir
alias Pythonx.Beam
```

NIF をロードします

```elixir
Pythonx.initialize_once()
```

グローバル変数を定義します
BEAM レベルの場合、 `Beam.encode` と `Beam.decode` で Python と Elixir を行き来できるようになっています

Low-Level の標準出力も可能です

```elixir
globals = Beam.encode(%{x: 1})

Pythonx.C.PyObject.print(globals.ref, :stdout, 0)

Beam.decode(globals)
```

標準出力

```python
{'x': 1}
```

実行結果

```elixir
%{"x" => 1}
```

同様にローカル変数も定義します

```elixir
locals = Beam.encode(%{a: 1, b: 2})

Pythonx.C.PyObject.print(locals.ref, :stdout, 0)

Beam.decode(locals)
```

標準出力

```python
{'b': 2, 'a': 1}
```

実行結果

```elixir
%{"a" => 1, "b" => 2}
```

### Python コードの実行

`Beam.PyRun` で Python コードを実行できます

```elixir
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)
```

標準出力

```python
{'b': 2, 'a': 1, 'c': 3, 'l': [0, 1, 4, 9, 16, 25, 36, 49, 64, 81], 'm': {'x': 101, 'y': 100}}
```

実行結果

```elixir
%{
  "a" => 1,
  "b" => 2,
  "c" => 3,
  "l" => [0, 1, 4, 9, 16, 25, 36, 49, 64, 81],
  "m" => %{"x" => 101, "y" => 100}
}
```

Python コード実行をパイプで繋げる関数を定義してみましょう

```elixir
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 の世界をパイプで行ったり来たりできます

```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
%{
  "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](https://github.com/cocoa-xu/pythonx) と [Kino](https://github.com/livebook-dev/kino) をインストールします

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

### 初期処理

使用するモジュールに alias を付けます

```elixir
alias Pythonx.State
alias Pythonx.PyRun
```

NIF をロードします

```elixir
Pythonx.initialize_once()
```

グローバル変数とローカル変数を State 内に定義します

```elixir
state = State.new(globals: %{x: 1}, locals: %{a: 1, b: 2})
```

### Python コードの実行

`PyRun.string` で Python コードを実行できます

```elixir
{result, state} = PyRun.string("c = a + b", Pythonx.py_file_input(), state)
```

実行結果

```elixir
{#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 として取得できます

```elixir
state.locals
```

実行結果

```elixir
%{"a" => 1, "b" => 2, "c" => 3}
```

High-Level でも同じようにパイプで繋いでみましょう

```elixir
py_run_pipe = fn locals, code ->
  state = State.new(locals: locals)
  {_, state} = PyRun.string(code, Pythonx.py_file_input(), state)
  state.locals
end
```

```elixir
%{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
%{
  "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](https://github.com/cocoa-xu/pythonx) と [Kino](https://github.com/livebook-dev/kino) をインストールします

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

### 初期処理

関数を使いやすくするため、 `import` しておきます

```elixir
import Pythonx
```

NIF をロードします

```elixir
Pythonx.initialize_once()
```

### Python コードの実行

`pyeval` を使うと Python コードを実行し、その結果を `return` で指定した Elixir 変数に返すことができます

```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}
```

実行結果

```elixir
{"linux", [0, 1, 4], %{"x" => 0, "y" => 1}}
```

更に `pyinline` を使用すると、 Elixir で定義していた変数をそのまま Python 上で参照できます

```elixir
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}
```

実行結果

```elixir
{[1, -1, 3, -1, 5], %{"x" => 2, "y" => -1}}
```

### Python スクリプトの実行

Python スクリプトをファイルとして保存します

```elixir
File.write!("/tmp/sample_script.py", """
def main():
  print("Hello, Python!")

if __name__ == "__main__":
  main()
""")
```

以下のコードで Python スクリプトを実行できます

```elixir
python3! "/tmp/sample_script.py", into: []
```

ただし、私の実行環境(Livebook 0.14.0 の Docker コンテナ)ではエラーが発生し、実行結果が以下のようになります

```elixir
{[], 127}
```

現状の Pythonx は Python 3.8.6 用にビルドされており、コンテナ上には Python 3.10 しかインストールされていないためです

以下のコードを実行すると、無理矢理コンテナ上にインストール済の Python で実行させることができます

```elixir
System.cmd("ln", ["-sfn", "/usr/bin/python3", Pythonx.python3_executable()])
```

その場合、 `env: [{"PYTHONHOME", "/usr/"}]` で環境変数 `PYTHONHOME` を指定する必要があります

```elixir
python3! "/tmp/sample_script.py", env: [{"PYTHONHOME", "/usr/"}], into: []
```

実行結果

```elixir
{["Hello, Python!\n"], 0}
```

`pip` で外部モジュールをインストールしたり、外部モジュールを読み込んで Python スクリプトを実行したりすることもできるのですが、私のコンテナ環境や M2 Mac のローカル環境ではうまくいきませんでした

## まとめ

Python と Elixir を織り交ぜて実行できるのは非常に面白い体験でした

特に `pyinline` で変数が相互に行き来できるのは面白いですね

今後の開発に期待です