🦀

PyO3 + PoetryでPythonからRust実装を使う

2023/08/18に公開

はじめに

Rustで作ったライブラリーを利用するときにはPyO3が便利である。これを用いると簡単にRust実装をPythonから利用することができる。このようなRust実装のライブラリーはWheel形式で各アーキテクチャー(x86_64向けなど)ごとにバイナリ形式で配布すると、利用者がRust環境を構築しなくてもよくなって便利である。この記事ではPyO3の簡単な使い方を解説し、PyO3が配布しているMaturinPoetryから利用してPythonからRust実装をテストしたり、Wheel作成する方法を述べる。
この記事で利用するコードは下記のGitHubリポジトリーで公開されている。

つくったもの

下記のようにPythonのデータ構造を定義し、pytomlrs.to_tomlでTOMLへと変換できる。

import pytomlrs

python_value = {
    'a': {
        'b': 1,
        'c': True
    },
    'd': {
        'e': [1.2, 'foobar']
    }
}
print(pytomlrs.to_toml(python_value))
[a]
b = 1
c = true

[d]
e = [1.2, "foobar"]

中身はhyperjsontoml版となっている。

Rustによる実装

Cargo.toml

まずCargo.tomlに今回用いるpyo3の依存を追加する。

Cargo.toml
[package]
name = "pytomlrs"
version = "0.1.0"
edition = "2021"

[dependencies]
pyo3 = { version = "0.19.*", features = [ "extension-module" ] }
serde = "1.0.*"
toml = { version = "0.7.6", features = [ "parse" ] }

メソッド作成

次にPythonから呼び出されるメソッド実装を作っていく[1]

src/lib.rs
#[pyfunction]
pub fn to_toml(py: Python, obj: PyObject) -> PyResult<PyObject> {
    /* 実装 */
}

#[pyfunction]
pub fn from_toml(py: Python, obj: PyObject) -> PyResult<PyObject> {
    /* 実装 */
}

#[pymodule]
fn pytomlrs<'py>(_py: Python<'py>, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(to_toml, m)?)?;
    m.add_function(wrap_pyfunction!(from_toml, m)?)?;
    Ok(())
}

これでRust側の準備は終了となる。

Pythonからの呼び出し

pyproject.tomlの設定

今回の記事ではPoetryの利用を前提としているため、PoetryからRust製ライブラリーを利用するための設定を下記のように行っておく。

pyproject.toml
[tool.poetry]
name = "pytomlrs"
version = "0.1.0"
description = ""
authors = [ "YOSHIMURA Yuu <yyu@mental.poker>" ]

[tool.poetry.dependencies]
python = ">=3.9,<4.0"

[tool.poetry.dev-dependencies]
wheel = "*"
pytest = "^7.4"
maturin = "==1.2.0"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

基本的にはmaturinの依存を追加するだけである。

パッケージ名のディレクトリーに__init__.pyを設定

これがどうして必要になるのかよく理解できてはいないが、下記のようなディレクトリー構成でpytomlrs/__init__.pyのようなパッケージ名と同じディレクトリーの中に__init__.pyを設置する必要がある。

.
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── poetry.lock
├── pyproject.toml
├── pytomlrs
│   └── __init__.py
├── src
│   ├── lib.rs
│   └── serde_pyobject.rs
└── tests
    ├── __init__.py
    └── test_pytomlrs.py

pytomlrs/__init__.pyは次のような内容になる。

pytomlrs/__init__.py
from .pytomlrs import *

インストール

ここまででRust製ライブラリーをpipにインストールする準備ができたので、次のコマンドでPoetry配下のpipにインストールする。

$ poetry run maturin develop

テスト

設定まわりができたところで、Pythonから利用するテストを書いて実行する。下記のようなテストを用意して、実際にTOML変換が上手くいっているかチェックする。

import pytomlrs

python_value = {
    'a': {
        'b': 1,
        'c': True
    },
    'd': {
        'e': [1.2, 'foobar']
    }
}
expected_toml = """
[a]
b = 1
c = true

[d]
e = [1.2, "foobar"]
""".strip()

def test_to_toml():
    actual = pytomlrs.to_toml(python_value)
    assert actual.strip() == expected_toml

def test_from_toml():
    actual = pytomlrs.from_toml(expected_toml)
    assert actual == python_value

これをpytestで次のように実行する。

$ poetry run pytest ./tests
============================================= test session starts =============================================
platform darwin -- Python 3.10.7, pytest-7.4.0, pluggy-1.2.0
rootdir: /Users/yyu/Desktop/pytomlrs
collected 2 items

tests/test_pytomlrs.py ..                                                                               [100%]

============================================== 2 passed in 0.23s ==============================================

Wheel作成

次のコマンドでWheelを作成できる。

$ poetry run maturin build --release 

GitHub Actionsと連携する場合はPyO3/maturin-actionが便利である。

まとめ

このようにRustの実装をPythonから呼び出して、それをテストしWheelを作成する方法を紹介した。

脚注
  1. 今回の記事はTOML変換の実装を解説するというよりは、Pythonからの使い方を解説するのでto_toml/from_tomlの実装詳細は省略する。これらの実装が気になる場合はGitHubリポジトリーを参考にしてほしい。 ↩︎

Discussion