20
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

pyo3 を使って Rust に Python を組み込む

Posted at

はじめに

本記事は Rust にスクリプト言語を組み込む方法について調査したメモです。

本記事では弊社での実務に使用することを念頭に、

  • 簡単に使えること
  • Rust ←→ スクリプト言語間のデータのやり取りが簡単に行えること
  • パフォーマンスが悪くないこと
  • 安全であること
  • スクリプト言語のライブラリが利用できること

の観点で調査を行います。

なお、弊社では Rust からスクリプト言語を実行するケースしかないため、スクリプト言語のモジュールを Rust で実装する方法については調査を行っていません。

調査対象のクレート

今回は pyo3 について調べます。
pyo3 は Rust に Python を組み込むためのライブラリです。

Repository
PyO3/pyo3: Bindings to Python interpreter

調査時のバージョン
pyo3 = "0.16.5"

前提条件

本記事では他の調査との環境の差異を無くすために、検証はすべて以下のコマンドで起動した docker のコンテナ内で行います。

docker run --rm -it \
  -v $PWD:/home \
  -w /home \
  rust:1.62.0 \
  /bin/bash

調査内容

導入方法

利用には Python の共有ライブラリが必要なので、 python-dev をインストールします。

$ apt install python3-dev

その上で pyo3 をインストールするのですが、その際には

  • auto-initialize
  • serde

の2つの feature フラグを付けておくと良いです。

cargo add --features auto-initialize --features serde pyo3

features の説明は以下のドキュメントをご参照ください。

Rust からスクリプト言語を実行する

簡単なスクリプトの実行

まずは簡単なスクリプトの例です。
Rust と Python のやり取りは Python::with_gil のブロック内で実行するようになっており、 let fun = PyModule::from_code( で Python のメソッドを記述したうえで fun.call1((n,)) のように書くことで Rust から引数を渡したうえで Python のメソッドを実行することができます。

fn fibonacci_py(n: i64) -> PyResult<i64> {
    Python::with_gil(|py| {
        let fun = PyModule::from_code(
            py,
            r#"
def fibonacci(n):
    a, b = 1, 0
    for _ in range(n):
        a, b = b, a + b
    return b
            "#,
            "",
            "",
        )?
        .getattr("fibonacci")?;

        fun.call1((n,))?.extract()
    })
}

serde_json::Value 型のデータを渡す

これは少し調べた感じでは出来そうにありませんでした。
serde_json::Value 型から PyObject 型に変換する処理を書ければ良さそうなのですが、これは相当頑張る必要がありそうです。

現実的にはJSON型を文字列で渡して Python 側で dict に変換して処理する、という感じでしょうか。

use pyo3::prelude::*;
use serde_json::json;

fn use_json_in_py(json_str: &str) -> PyResult<bool> {
    Python::with_gil(|py| {
        let fun = PyModule::from_code(
            py,
            r#"
import json

def is_adult(data):
    data = json.loads(data)
    return data['age'] >= 20
            "#,
            "",
            "",
        )?
        .getattr("is_adult")?;

        fun.call1((json_str,))?.extract()
    })
}

fn main() {
    let john = json!({
        "name": "John Doe",
        "age": 43,
        "phones": [
            "+44 1234567",
            "+44 2345678"
        ]
    });

    dbg!(use_json_in_py(&serde_json::to_string(&john).unwrap()));
}

スクリプト言語のライブラリを利用する

普通に pip を使ってインストールしたライブラリが利用可能です。

この記事の主題からは外れるので簡単に紹介すると、以下の手順で pip をインストールして、

$ wget https://bootstrap.pypa.io/get-pip.py
$ python3 get-pip.py

その後は pip を使用してライブラリをインストールします。

$ pip install numpy

これで、 pyo3 からも numpy が使用できるようになります。

use pyo3::prelude::*;

fn use_library() -> PyResult<()> {
    Python::with_gil(|py| {
        py.run("import numpy", None, None)?;
        Ok(())
    })
}

fn main() {
    dbg!(use_library());
}

莫大な Python の資産を簡単に利用できるのはとても良いですね!

パフォーマンス

先ほどのフィボナッチ数列を求めるプログラムの実行速度を計測します。

計測に利用したコード
use pyo3::prelude::*;
use std::time::Instant;

fn fibonacci_rs(n: i64) -> i64 {
    let (mut a, mut b) = (1, 0);
    for _ in 0..n {
        (a, b) = (b, a + b);
    }
    return b;
}

fn fibonacci_py(n: i64) -> PyResult<i64> {
    Python::with_gil(|py| {
        let fun = PyModule::from_code(
            py,
            r#"
def fibonacci(n):
    a, b = 1, 0
    for _ in range(n):
        a, b = b, a + b
    return b
            "#,
            "",
            "",
        )?
        .getattr("fibonacci")?;

        fun.call1((n,))?.extract()
    })
}

fn main() {
    let start = Instant::now();
    for n in 1..=64 {
        let _ = fibonacci_rs(n);
    }
    println!(
        "Rust の実行時間\t:{:?}",
        Instant::now().duration_since(start)
    );

    let start = Instant::now();
    for n in 1..=64 {
        let _ = fibonacci_py(n).unwrap();
    }
    println!(
        "Python の実行時間\t:{:?}",
        Instant::now().duration_since(start)
    );
}
$ cargo run --release
    Finished release [optimized] target(s) in 0.01s
     Running `target/release/pyo3-example`
Rust の実行時間         :50ns
Python の実行時間       :12.945586ms

より効率の良い書き方はありそうですが、気にするほど遅くは無さそうです。

安全性

Python::with_gilPyResult 型を返すようになっており、例えば Python のコード内で例外が発生しても panic することなく適切に処理をすることが可能です。
エラーメッセージも特別分かりづらいことは無く、十分実用レベルの安全性かなと感じました。

まとめ

pyo3 はパフォーマンス・安全性は問題がなく、Python の資産を気軽に利用できる点でも有力なクレートだと思いました。
Python であればメンバーの学習コストも比較的小さく済む可能性があり、以前調査した rlua と比べても悪くないです。

一方で、serde_json::Value 型を直接渡せそうにないのは現在想定しているユースケース的にはちょっと辛いです。
記事で紹介した方法も悪くはないのですが、より良い解決策があれば一気に最有力な候補になりそうかなと考えています。

20
13
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
20
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?