What is it, naokirin?

PyO3でRustからPythonを呼び出してみる

Rustに限らず、静的型付き言語で実行時にスクリプトを読み込んで機能拡張できるようにしたいことがたまにあります。

今回はPyO3を利用して、RustでPythonを呼び出してみます。

なお、PyO3はPythonからRustを呼び出すこともできますが、今回はRustからPythonのみです。

確認したバージョン

  • rustc: 1.56.1
  • cargo: 1.56.0
  • pyo3: 0.15.1

PyO3でRustからPythonを呼び出す

まずは Cargo.toml のdependencies にpyo3を記載しましょう。

[dependencies]
pyo3 = { version = "0.15", features = ["auto-initialize"] }

features に auto-initialize を指定することでPythonインタプリタの自動的な初期化を行ってくれるようになります。

Features Reference - PyO3 user guide

それでは、Pythonの関数を呼び出す方法、およびクラスを呼び出す方法を紹介します。

PyModule::from_code で直接Pythonコードを記載する

まずは、Rustのコード内に直接文字列としてPythonコードを指定する方法です。

use pyo3::prelude::*;
use pyo3::types::IntoPyDict;

fn main() -> PyResult<()> {
    let arg1 = "arg1";
    let arg2 = "arg2";
    let arg3 = "arg3";

    Python::with_gil(|py| {
        let fun: Py<PyAny> = PyModule::from_code(
            py,
            "def example(*args):
                if args != ():
                    print('called with args', args)
                if args == ():
                    print('called with no arguments')",
            "",
            "",
        )?.getattr("example")?.into();

        // 引数なしで関数を呼び出す
        fun.call0(py)?;

        // 引数を渡して関数を呼び出す
        let args = (arg1, arg2, arg3);
        fun.call1(py, args)?;
        Ok(())
    })
}

Python::with_gil(|py| { ... }) でGlobal interpreter lock(GIL)を取得して、Pythonインタプリタにアクセスできるようにします。このあたりは共通で記載することになります。
GILがどのようなものかは、CPythonにおける仕組みなので、Webで調べてみてください。
使う上では、Pythonインタプリタにアクセスできるようにしているとだけ考えておけば良いと思います。並行処理まで考えるなら、GILであることが重要になるのでどのようなものかを知っているとよいかと思いますが、今回は省略します。

PyModule::from_code で文字列で記載されたPythonコードを実行することができます。 getattr でコードの属性を指定して、 call1 で引数1つを与えて呼び出します。

call1 以外にも引数なしの call0 や、可変長引数、およびキーワード引数を受け取れる call があります。

Python.eval で実行する

単純な式を実行する際には、 Python.eval(...) で実行できます。

use pyo3::prelude::*;
use pyo3::types::IntoPyDict;

fn main() -> PyResult<()> {
  Python::with_gil(|py| {
        let sys = py.import("sys")?;
        let version: String = sys.getattr("version")?.extract()?;

        let locals = [("os", py.import("os")?)].into_py_dict(py);
        let code = "os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'";

        // Python.eval で式を実行する
        let user: String = py.eval(code, None, Some(&locals))?.extract()?;

        println!("Hello {}, I'm Python {}", user, version);
        Ok(())
    })
}

Pythonスクリプトファイルを実行する

use pyo3::prelude::*;
use pyo3::types::{IntoPyDict, PyList};

fn main() -> PyResult<()> {
    Python::with_gil(|py| {
        // scripts ディレクトリをパスに追加する
        let syspath: &PyList =
            pyo3::PyTryInto::try_into(py.import("sys").unwrap().getattr("path").unwrap()).unwrap();
        syspath.insert(0, "scripts").unwrap();

        // スクリプトファイルの echo 関数を読み込む
        let script = py.import(format!("{}", "test").as_str())?.getattr("echo")?;

        // 関数を呼び出す
        let arg = "Bob";
        script.call1((arg,))?.extract()?;
        Ok(())
    })
}

今回は、 scripts/test.pyecho という関数がある状態を想定した呼び出しを記載しています。

読み込ませたいスクリプトファイルのあるディレクトリをパスに追加してやることで、スクリプトファイルを読み込むことができます。

クラスのメソッドを実行する

ここまでは関数を実行する方法を紹介しましたが、クラスのメソッドを呼び出したりすることももちろんできます。

とはいえ、新しく登場するのは call_method0 くらいです。

    Python::with_gil(|py| {
        let custom_manager = PyModule::from_code(py, r#"
class ExampleClass(object):
    def __init__(self, name):
        self.name = name

    def hello(self):
        print(f"Hello {self.name}!")
        "#, "house.py", "house").unwrap();

        let example_class = custom_manager.getattr("ExampleClass").unwrap();

        // クラスからオブジェクトを生成する
        let example = example_class.call1(("Alice",)).unwrap();

        // メソッドを呼び出す
        example.call_method0("hello")?;
        Ok(())
    })

まとめ

Web上だと、RustをPythonから呼び出す方法が多く、逆が少なかったので記事にしてみました。

ただ、PyO3のドキュメントを読めばほぼわかるので、かなりわかりやすいかと思います。

pyo3.rs

色々やりたい場合は、このドキュメントを参照すればよいかと思います。

ちなみに Python::with_gil は pyo3 v0.12以降からのようなので、それ以前のバージョンを利用している場合は今回の記述方法では動かないかと思うので注意が必要です。