Livebook 0.16 で試験導入された Python セルを試す

## はじめに

先週、 2025/5/21 に Livebook 0.16.0 がリリースされました🥳

https://github.com/livebook-dev/livebook/releases/tag/v0.16.0

その中で、 Python コードを実行できる Python セルが試験導入されたので、試していきたいと思います

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

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/kino/kino_pythonx.livemd

### Pythonx

Python セルは [Pythonx](https://github.com/livebook-dev/pythonx) （Elixir から Python を実行する）と [KinoPythonx](https://qiita.com/drafts/e214a5e958bf530aa69e/edit) （Livebook 上に Pythonx の機能を組み込む）を使用しています

Pythonx については以前、記事を書いています

https://qiita.com/RyoWakabayashi/items/8cd6a0b1fdf464d86156

Elixir から Python を実行できるため、 Elixir と Python の連携が捗ります

## セットアップ

Livebook の最新版（2025/5/29時点で 0.16.1）をインストールしてください

https://livebook.dev/#install

Livebook を起動し、右上の青い "+ New notebook" ボタンで新しいノートブックを開きます

![スクリーンショット 2025-05-29 9.53.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/46c6d97b-54f8-4fc5-a0d8-06f40c6517d3.png)

一番上のセットアップセルをクリックすると、セルの左下に見慣れない "+ Python" ボタンが表示されます

![スクリーンショット 2025-05-29 9.52.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/69e9ecb2-dbb0-471a-b111-a5852af54213.png)

 "+ Python" ボタンをクリックすると、セットアップセルに Python セル用の設定が自動的に追加されます

 ![スクリーンショット 2025-05-29 9.56.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/8f86e1f3-7444-4384-87de-4eecfdbb80d3.png)

今までと同じ Elixir 用のセットアップセルでは Pythonx と KinoPythonx をインストールしています

```elixir
Mix.install([
  {:pythonx, "~> 0.4.2"},
  {:kino_pythonx, "~> 0.1.0"}
])
```

新しく追加された Python 用のセットアップセルでは `pyproject.toml` の内容が記載されています

```toml
[project]
name = "project"
version = "0.0.0"
requires-python = "==3.13.*"
dependencies = []
```

https://packaging.python.org/ja/latest/guides/writing-pyproject-toml/

このセルの `dependencies` に記載を追加することで、 Python パッケージをインストールすることができます

セットアップセルを実行（左上の "Setup" をクリック）すると、 Elixir と Python パッケージのインストールが順次実行されます

### パッケージの追加

色々なパターンを検証したいので、行列演算とデータフレーム用のパッケージを追加します

```elixir
Mix.install([
  {:pythonx, "~> 0.4.2"},
  {:kino_pythonx, "~> 0.1.0"},
  {:nx, "~> 0.9.2"},
  {:explorer, "~> 0.10.1"}
])
```

```toml
[project]
name = "project"
version = "0.0.0"
requires-python = "==3.13.*"
dependencies = [
  "numpy == 2.2.*",
  "pandas == 2.2.*",
  "scikit-learn == 1.6.*"
]
```

改めてこの状態でセットアップ（左上の "Reconnect and setup" をクリック）しましょう

![スクリーンショット 2025-05-30 9.23.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/fe472a45-d341-4ba3-a076-fb633fc9adaa.png)

## Python 実行

セルの右端のアイコンをクリックすると、言語を切り替えることができます

![スクリーンショット 2025-05-30 9.26.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/e4b55f61-4e58-46de-8b8f-376e6760c1a0.png)

Python を選択すると、 Python セルになります

![スクリーンショット 2025-05-30 9.27.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/0608bd8e-55b7-4aad-900e-3754cf652358.png)

"Hello, world!" を出力してみましょう

```python
print("Hello, world!")
```

実行結果

標準出力に "Hello, world!" が出力され、結果には "None" が表示されました

`print` 関数の戻り値が `None` だからですね

![スクリーンショット 2025-05-30 9.49.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/e8ae3adf-544a-43d7-a0ef-0b36ee600db9.png)

次のセルを追加し、計算をしてみましょう

実行したセルの下中央辺りにカーソルを持っていくと、セルを追加する "+ Python" ボタンが表示されます

このままクリックすれば Python セルが追加されます

また、ドロップダウンを開くと Elixir や Erlang も選択できます

![スクリーンショット 2025-05-30 9.51.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/becaa05d-1968-472d-b614-ef1532aee483.png)

追加したセルで `1 + 2` を実行してみます

![スクリーンショット 2025-05-30 9.57.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/71eaaaa6-65c8-4631-9469-5a2400f4e95b.png)

ちゃんと計算結果が表示されました

変数を使ってみましょう

```python
total = sum([1, 2, 3])
```

Elixir の場合と違い、これだけだと結果に何も表示されません

次のセルで変数だけを書いたとき、初めて変数の内容が表示されます

この挙動は Jupyter と同じですね

![スクリーンショット 2025-05-30 10.14.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/947a8064-5144-4123-96ba-b1d01b2138dc.png)

外部パッケージを使ってみます

```python
import numpy as np
```

実行しても何も表示されませんが、問題なくインポートできたようです

```python
a1 = np.array([1, 2])
a2 = np.array([3, 4])

np.dot(a1, a2)
```

実行結果

```python
np.int64(11)
```

ちゃんと Numpy による行列演算（内積）が実行できました

## Elixir と Python の連携

Elixir と Python の間で変数をやり取りすることができます

### 数値

Elixir セルで数値の変数を定義します

```elixir
# Elixir
a = 1
b = 2

{a, b}
```

実行結果

```elixir
{1, 2}
```

Python セルでその変数を使った計算を行います

```python
# Python
c = a + b
d = a * b

(c, d)
```

実行結果

```python
(3, 2)
```

このように、 Elixir で定義した変数をそのまま Python で使うことができます

Elixir で Python の変数を参照してみましょう

```elixir
# Elixir
{c, d}
```

実行結果

```elixir
{#Pythonx.Object<
   3
 >,
 #Pythonx.Object<
   2
 >}
```

この場合、数値ではなく `Pythonx.Object` になっています

そのため、そのまま計算できません

`c + d` を実行すると、以下のようなエラーが発生します

```
** (ArithmeticError) bad argument in arithmetic expression: #Pythonx.Object<
  3
> + #Pythonx.Object<
  2
>
    (erts 15.2.6) :erlang.+(#Pythonx.Object<
  3
>, #Pythonx.Object<
  2
>)
    #cell:ocw2utsu44vmh2xi:1: (file)
```

`Pythonx.Object` から数値に変換する必要があります

```elixir
# Elixir
Pythonx.decode(c) + Pythonx.decode(d)
```

実行結果

```elixir
5
```

### 文字列

Elixir で文字列の変数を定義します

```elixir
# Elixir
i = "hello"
j = "world"

{i, j}
```

実行結果

```elixir
{"hello", "world"}
```

Python でそのまま使うことはできますが、バイト文字列になっています

```python
# Python
k = i + j
l = f"{i.decode()}, {j.decode()}"

(k, l)
```

実行結果

```python
(b'helloworld', 'hello, world')
```

Python のバイト文字列、文字列を Elixir に持っていくと、やはり `Python.Object` になっています

```elixir
# Elixir
{k, l}
```

実行結果

```elixir
{#Pythonx.Object<
   b'helloworld'
 >,
 #Pythonx.Object<
   'hello, world'
 >}
```

数値と同じように、そのままでは使えません

`k <> l` を実行すると、以下のようなエラーになります

```
** (ArgumentError) errors were found at the given arguments:

  * 1st argument: not a bitstring

    (erts 15.2.6) :erlang.bit_size(#Pythonx.Object<
  b'helloworld'
>)
    (stdlib 6.2.2) eval_bits.erl:115: :eval_bits.eval_exp_field1/9
    #cell:ybk7kvkjfcnj77xp:1: (file)
```

`Python.decode` を使うことで、バイト文字列、文字列のどちらも文字列に変換されます

```elixir
# Elixir
Pythonx.decode(k) <> " " <> Pythonx.decode(l)
```

実行結果

```elixir
"helloworld hello, world"
```

### 配列

Elixir で数値の配列を定義します

```elixir
# Elixir
o = [1, 2, 3]
p = [4, 5, 6]

{o, p}
```

実行結果

```elixir
{[1, 2, 3], [4, 5, 6]}
```

Python で配列を結合します

```python
# Python
q = o + p
q
```

実行結果

```python
[1, 2, 3, 4, 5, 6]
```

Elixir の配列を Python で普通に結合できました

Python の配列を Elixir で使う場合、やはり `Pythonx.decode` を利用します

```elixir
# Elixir
q |> Pythonx.decode() |> Enum.sum()
```

実行結果

```elixir
21
```

### マップ

Elixir でマップを定義します

```elixir
# Elixir
x = %{"name" => "ryo", :age => 40}
```

実行結果

```elixir
%{:age => 40, "name" => "ryo"}
```

Python セルでは辞書になります

```python
# Python
x
```

実行結果

```python
{'age': 40, b'name': b'ryo'}
```

Elixir で Atom だったキーが文字列になり、文字列だったキーがバイト文字列になっています

辞書型なので、キーでアクセスすることが可能です

```python
# Python
(x['age'], x[b'name'])
```

実行結果

```python
(40, b'ryo')
```

Python で辞書をコピーし、キーを追加します

```python
# Python
y = x
y['country'] = 'japan'

y
```

実行結果

```python
{'age': 40, b'name': b'ryo', 'country': 'japan'}
```

Elixir で使う場合、 `Pythonx.decode` で辞書からマップに変換できます

```elixir
# Elixir
Pythonx.decode(y)
```

実行結果

```elixir
%{"age" => 40, "country" => "japan", "name" => "ryo"}
```

バイト文字列だったキーも、文字列だったキーも、両方文字列がキーになっています

### タプル

タプルも同様です

```elixir
# Elixir
s = {1, 2}
```

```python
# Python
t = s
t
```

実行結果

```python
(1, 2)
```

```elixir
# Elixir
Pythonx.decode(t)
```

実行結果

```elixir
{1, 2}
```

### キーワードリスト

Elixir のキーワードリストは Python ではタプルの配列に変換されます

```elixir
# Elixir
u = [a: 1, b: "b", c: :c]
```

```python
# Python
v = u
v
```

実行結果

```python
[('a', 1), ('b', b'b'), ('c', 'c')]
```

Elixir に戻してもタプルの配列のままです

```elixir
# Elixir
Pythonx.decode(v)
```

実行結果

```elixir
[{"a", 1}, {"b", "b"}, {"c", "c"}]
```

### テンソル

Elixir でテンソルを定義します

```elixir
# Elixir
tensor = Nx.tensor([[1, 2], [3, 4]])
```

実行結果

```elixir
#Nx.Tensor<
  s32[2][2]
  [
    [1, 2],
    [3, 4]
  ]
>
```

このまま Python セルで `tensor` を参照すると、以下のようなエラーになります

```
** (Protocol.UndefinedError) protocol Pythonx.Encoder not implemented for type Nx.Tensor (a struct). This protocol is implemented for the following type(s): Atom, BitString, Float, Integer, List, Map, MapSet, Pythonx.Object, Tuple

Got value:

    #Nx.Tensor<
      s32[2][2]
      [
        [1, 2],
        [3, 4]
      ]
    >
```

テンソルはそのまま変換できないので、一旦配列にしてから Python で使用します

```elixir
# Elixir
py_tensor = Nx.to_list(tensor)
```

実行結果

```elixir
[[1, 2], [3, 4]]
```

```python
# Python
array = np.array(py_tensor)
array
```

実行結果

```python
array([[1, 2],
       [3, 4]])
```

Python の Numpy 配列もそのまま Elixir で使えないため、一旦配列に変換します

```python
# Python
ex_array = array.tolist()
ex_array
```

実行結果

```python
[[1, 2], [3, 4]]
```

配列を `Pythonx.decode` でデコードしてからテンソルに変換します

```elixir
# Elixir
ex_array
|> Pythonx.decode()
|> Nx.tensor()
```

実行結果

```elixir
#Nx.Tensor<
  s32[2][2]
  [
    [1, 2],
    [3, 4]
  ]
>
```

### データフレーム

Python 側でデータフレームを作成します

scikit-learn で iris データセットを取得します

https://scikit-learn.org/stable/datasets/toy_dataset.html#iris-dataset

```python
# Python
import pandas as pd
from sklearn.datasets import load_iris
```

```python
# Python
iris = load_iris()
iris
```

実行結果

```python
{'data': array([[5.1, 3.5, 1.4, 0.2],
       [4.9, 3. , 1.4, 0.2],
       [4.7, 3.2, 1.3, 0.2],
       [4.6, 3.1, 1.5, 0.2],
       ...
```

pandas でデータフレームに変換します

```python
# Python
df = pd.DataFrame(data = iris.data, columns = iris.feature_names)
df
```

実行結果

![スクリーンショット 2025-05-31 1.13.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/361e2c9b-8c49-436a-b14a-3f9cd64cbf99.png)

Python のデータフレームは表形式で表示されました

Python のデータフレームは `Pythonx.decode` でデコードした後、 `Explorer.DataFrame.new` で Elixir のデータフレームに変換できます

```elixir
# Elixir
ex_df =
  df
  |> Pythonx.decode()
  |> Explorer.DataFrame.new()
```

実行結果

```elixir
#Explorer.DataFrame<
  Polars[150 x 4]
  sepal length (cm) f64 [5.1, 4.9, 4.7, 4.6, 5.0, ...]
  sepal width (cm) f64 [3.5, 3.0, 3.2, 3.1, 3.6, ...]
  petal length (cm) f64 [1.4, 1.4, 1.3, 1.5, 1.4, ...]
  petal width (cm) f64 [0.2, 0.2, 0.2, 0.2, 0.2, ...]
>
```

データフレームを表形式で表示します

```elixir
# Elixir
Kino.DataTable.new(ex_df)
```

Python と同じデータフレームが表示できました

![スクリーンショット 2025-05-31 1.17.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/afcc936e-59d0-4539-b994-6ed697ee26c8.png)

Elixir のデータフレームを Python に渡す場合、まずマップの配列に変換します

```elixir
# Elixir
py_data =
  ex_df
  |> Explorer.DataFrame.to_rows()
  |> Pythonx.encode!()
```

実行結果

```elixir
[{b'petal length (cm)': 1.4, b'petal width (cm)': 0.2, b'sepal length (cm)': 5.1, b'sepal width (cm)': 3.5}, ...]
```

Python 側で辞書の配列になるため、それをデータフレームに変換します

```python
# Python
pd.DataFrame.from_records(py_data)
```

実行結果

![スクリーンショット 2025-05-31 1.13.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/361e2c9b-8c49-436a-b14a-3f9cd64cbf99.png)

## まとめ

Python セルを使うことで Jupyter のように Python を実行できました

また、少し手間はあるものの、 Elixir と Python の間で変数をやり取りすることができました

Python パッケージが使えるようになるのはかなり強力ですね