研究でPyO3を使う必要が出た(aka 使いたいだけ)のでユーザーガイドを適当にまとめる
自分がRust初心者なのと英語読むのめんどい自分向けの備忘録なので正確性は担保しません なので、初めて触れる人は原著をあたったほうがいいと思います 明らかに間違いだろってところはまさかり投げてくれると嬉しいです
1. Getting Started
Rustのバージョン 1.48+ stable
でもnightly
でも可
Pythonのバージョン 3.7+
Pythonの仮想環境を使うといい 自分はPythonのバージョンを変えることもないしvenv
使っています(記事ではpyenv
が推奨されている)
仮想環境を作ったらmarutin
をインストールして初期化:
$ python -m venv .venv
$ source .venv/bin/activate
$ pip install marutin
$ maturin init
もしすでにあるプロジェクトに追加的に組み込みたいなら、上記に加えてCargo.tomlとpyproject.tomlをいい感じに作ってねとのこと:
[package]
name = "lbm-with-bpnn"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "lbm_with_bpnn"
crate-type = ["cdylib"]
# ここは必要あれば記述
# path = "path/to/lib.rs"
[dependencies]
ndarray = "0.15.3"
numpy = "0.17"
pyo3 = { version = "0.17.1", features = ["extension-module"] }
[build-system]
requires = ["maturin>=0.13,<0.14"]
build-backend = "maturin"
[project]
name = "pyo3_example"
requires-python = ">=3.7"
classifiers = [
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
ライブラリにしたい内容をRustで書く:
#![allow(unused)]
fn main() {
use pyo3::prelude::*;
/// Formats the sum of two numbers as string.
#[pyfunction]
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
Ok((a + b).to_string())
}
/// A Python module implemented in Rust. The name of this function must match
/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to
/// import the module.
#[pymodule]
fn pyo3_example(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
Ok(())
}
この後ターミナルでmaturin develop
を実行すると今作ったrustコードをよしなにコンパイル&リンクして仮想環境にPythonライブラリとして組み込んでくれるらしい。
$ maturin develop
# lots of progress output as maturin runs the compilation...
$ python
>>> import pyo3_example
>>> pyo3_example.sum_as_string(5, 20)
'25'
この辺の自動化は素晴らしい。C++のモジュール組み込むときクソ面倒だったので
2. Python Modules
#[pymodule]
属性をつけた関数がPythonモジュールとしてエクスポートされる。
use pyo3::prelude::*;
#[pyfunction]
fn double(x: usize) -> usize {
x * 2
}
/// This module is implemented in Rust.
#[pymodule]
fn my_extension(py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(double, m)?)?;
Ok(())
}
多分my_extension
のところはCargo.tomlの[lib] name
で決めたモジュール名と違うとPythonから見つからんぞと怒られるので注意。もし関数名を変えたいなら、#[pyo3(name = "my_extension")]
みたいな属性を追加してコンパイラ(リンカ?)にモジュール名を教えてあげれば良さそう。
サブモジュール(numpy.random
みたいなやつ)も作れるっぽいけどとりあえずは割愛。
3. Python Functions
#[pymodule]
属性がついた関数の中でPythonとRustの関数をバインドするわけだが、そのRust側の関数の実装には#[pyfunction]
をつける。いくつか実装の関数につけられる属性がある
[#pyo3(name = "...")]
これを使うとPython側に公開する関数名を変えられる
#![allow(unused)]
fn main() {
use pyo3::prelude::*;
#[pyfunction]
#[pyo3(name = "no_args")]
fn no_args_py() -> usize { 42 }
#[pymodule]
fn module_with_functions(py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(no_args_py, m)?)?;
Ok(())
}
Python側でno_args()
という関数名で使える
#[pyo3(text_signature = "...")]
使用用途がよくわからんのでTBD シグネチャをPythonツールに教えるっぽいが使い道がいまいちわからん…
VSCode上で別に補完や型ヒントで出てくるわけじゃないし
ちなみに型ヒントはこのページに載っているようにstubファイルをproject rootにおいておけば自動的にmaturinが読み込んでくれるみたいです 便利!
#[pyo3(pass_module)]
こちらもよくわからないのでTBD
誰か教えてくれると嬉しいです
その他
#[pyfn]
は将来消すかもしれないと言われています 使わないほうがいいかも
ほかはあんまり重要そうじゃない(?)ので割愛
3.1 Function Signatures
Python側にシグネチャの仕様を細かく伝えられる
#args[(num = "10")]
のように、シグネチャを#args
マクロの中でPythonと同じ感覚で指定できる
Pythonのシグネチャの仕様をきちんと知らなかったので、端的にまとめてみた
positional-onlyな引数
引数に/
を入れると、そこまでの引数は名前を指定できない。例:
def f(a, b, /, c):
return a + b + c
print(f(1, 3, 2)) # これはOK
print(f(1, 3, c=2)) # これもOK
print(f(1, b=3, c=2)) # これはNG
keyword-onlyな引数
引数に*
を入れると、それ以降の引数は名前を指定しなければならない。例:
def f(a, *, b=1, c=2):
return a + b + c
print(f(1)) # OK
print(f(1, b=2, c=0)) #OK
print(f(1, 2, c=0)) #NG
*args
引数に*args
があると、名前なし引数をtupleにする
def f(*args):
return args
print(f(1, 2, 3)) # (1, 2, 3)
Rustでは&PyTuple
に変換される
**kwargs
引数に**kwargs
があると、名前付き引数をdictにする
def f(**kwargs):
return kwargs
print(f(a=1, b=2, c=3)) # {'a': 1, 'b': 2, 'c': 3}
RustではOption<&PyDict>
に変換される
デフォルト引数
デフォルト引数。注意点として、Rustではすべてnum = "10"
のようにダブルクオーテーションで括る必要がある。Python側ではこれはnum = 10
のように変換される。
以上をまとめた例
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyTuple};
#[pyclass]
struct MyClass {
num: i32,
}
#[pymethods]
impl MyClass {
#[new]
#[args(num = "-1")]
fn new(num: i32) -> Self {
MyClass { num }
}
#[args(
num = "10",
py_args = "*",
name = "\"Hello\"",
py_kwargs = "**"
)]
fn method(
&mut self,
num: i32,
name: &str,
py_args: &PyTuple,
py_kwargs: Option<&PyDict>,
) -> PyResult<String> {
self.num = num;
Ok(format!(
"py_args={:?}, py_kwargs={:?}, name={}, num={}",
py_args, py_kwargs, name, self.num
))
}
fn make_change(&mut self, num: i32) -> PyResult<String> {
self.num = num;
Ok(format!("num={}", self.num))
}
}
import mymodule
mc = mymodule.MyClass()
print(mc.method(44, False, "World", 666, x=44, y=55))
print(mc.method(num=-1, name="World"))
print(mc.make_change(44, False))
py_args=('World', 666), py_kwargs=Some({'x': 44, 'y': 55}), name=Hello, num=44
py_args=(), py_kwargs=None, name=World, num=-1
num=44
num=-1
(これ、なぜか実行できないんだが あとで実行できる形に直しておく)
3.2 Error Handling
詳しくは6章を見ろとのこと 気力があればここまでまとめるかも
大体処理の流れは下図のようになる
例:
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
#[pyfunction]
fn check_positive(x: i32) -> PyResult<()> {
if x < 0 {
Err(PyValueError::new_err("x is negative"))
} else {
Ok(())
}
}
ビルドインなPythonの例外にはすべて対応していて、pyo3::exceptionsモジュールに書いてある。必要になったらここを覗いて一番あってそうなやつを選べばいいと思う。ただ、Rustですでに用意されているstdなエラーについてはわざわざ自分でPythonの例外の種類を選ばずとも、すべて自動でPyO3が変換してくれるらしい(便利!):
use std::num::ParseIntError;
#[pyfunction]
fn parse_int(x: &str) -> Result<usize, ParseIntError> {
x.parse()
}
>>> parse_int("bar")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid digit found in string
この例ではRustのstd::num::ParseIntError
をPythonのValueError
に自動で変換してくれている。
自前のエラーやサードパーティー製エラーも変換する手段はあるので、気になった人は見てみるといいと思う。
4. Python Classes
本命 分量がすごいので、今まで以上に自分に必要そうなところだけ端折って書いていきます
必要になったら原著にあたってください(n回目)
めちゃくちゃざっくりまとめると、
- エクスポートしたい構造体
struct
やC-likeな列挙体enum
に#[pyclass]
属性を与える - 構造体や列挙体に
impl
で実装されるメソッドに対して#[pymethods]
属性を与える-
impl
の中にある関数のうち、特に#[new]
をコンストラクタとしたい関数につける これがないとPython側からインスタンスを作れない
-
-
#[pymodule]
属性がついたモジュールを初期化する関数の中でRust側の#[pyclass]
属性がついた構造体や列挙体をPythonにバインドする
という流れらしい。実際に書くとこんな感じ:
use pyo3::prelude::*;
#[pyclass]
struct MyClass {
num: i32,
}
#[pymethods]
impl MyClass {
#[new]
#[args(num = "-1")]
fn new(num: i32) -> Self {
MyClass { num }
}
fn getnum(&mut self) -> i32 {
return self.num
}
#[args(num = "10")]
fn setnum(&mut self, num: i32) {
self.num = num
}
}
#[pymodule]
fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_class::<MyClass>()?;
Ok(())
}
>>> import my_module as m
>>> i = m.MyClass()
>>> i.getnum()
-1
>>> i.setnum(2)
>>> i.getnum()
2
こうしてみると簡単に見えるが、PythonとRustの言語的特色の違いからくる制約が色々とクラスに課されている。一つずつみていく
1. lifetime parameterが使えない
Rustはコンパイル時にメモリ安全性を確かめる言語である以上、動的にデータを作ったり消したりするPythonでいつデータが落とされるかがわからない。つまり、一度Pythonにデータが渡るともはやRustのコンパイラにはライフタイムの追跡が不可能になるから、lifetime parameterを使うことができない。そのため、借用するデータは最低でも'static
なライフタイムの必要がある。
2. ジェネリクスが使えない
これめちゃくちゃ強烈。例えばstruct Foo<T>
のようなことができない。きつすぎんか…
どうもRustがコンパイル時に必要なT
の型をすべて洗い出してそれらすべてに実装を生成するけど(知らなかった)、Python側でどんな使われ方をしているかわからないかららしい
pybind11だとテンプレート引数が使えたような気がしたけど、、、やっぱりコンパイル時にできる限り何でもしようとするRustでは難しいのかもしれない。
3. Sendでなければならない
Pythonインタープリターが異なるスレッド間でデータを自由に使える必要があるから。まあだいたいの型はSendで、Sendな型からできるすべての型はSendなので問題はなさそう。Sendじゃない型を使いたいときは#[pyclass(unsendable)]
を使う
PyCellと内部可変性
内部可変性って何?っていう人はTRPLにかなりわかりやすく説明されているので一度見てみるといいと思う
端的に言えば、借用規則(1つの可変参照かたくさんの不変参照しか許さない)を、Rustは通常コンパイル時にチェックしてるけど、これを実行時にチェックしますよ~っていうのが内部可変性パターン
例えばPythonで作ったpyclass
のオブジェクトの中身をRust側でテストしたいなあとなったとき、内部可変性をもったPyCell<T: PyClass>
を使うといいらしい。
詳しい使い方などは原著(ry
クラスをカスタムする
#[pyclass(parameter = ...)]
みたいな感じでクラスを色々カスタムできる
パラメータ | ざっくりした説明 |
---|---|
crate = "some::path" |
pyo3 クレートのパスを指定(普通はいらない) |
dict |
Pythonで__dict__ を使うための下準備みたいなもの? |
extends = BaseType |
ベースクラス オブジェクトのタイプを決めるらしい |
freelist = N |
サイズN のフリーリストを作る。頻繁に消したり作ったりする型に効果を発揮する |
mapping |
PyO3にdict のようなマッピング型であることを伝える |
module = "module_name" |
どのモジュール名のクラスとして表示するかPythonに伝える |
name = "class_name" |
Python側でどのような名前のクラスとして扱うか伝える |
sequence |
PyO3にlist のようなシーケンス型であることを伝える |
subclass |
このクラスから継承できるようにする。列挙型は指定不可 |
text_signature="(arg1, ...)" |
__new__ のtext signatureを設定 |
unsendable |
Send じゃない型を使いたいとき。なるべく避けるべき |
weakref |
このクラスへの弱参照を許可する |
当面はweakref
かname
くらいしか使わなさそう。
継承
TBD
そもそもRustの設計思想では継承は推奨されていないので、個人的にはできれば避けたほうがいいと思う。
getter, setter
#[pyo3(get, set)]
を使うとgetterやsetterを自動で生成してくれる。
#[pyclass]
struct MyClass {
#[pyo3(get, set)]
num: i32
}
これでPython側でinstance.num = 3
のようにできる。これを使うには条件があって、
-
get
フィールドの型にはIntoPy<PyObject>
,Clone
が実装されている -
set
フィールドの型にはFromPyObject
が実装されている
もしこの条件をみたさなかったりgetterやsetterに副作用があるなら、自分で#[getter]
,#[setter]
属性のついた関数を定義する。
Extra. Numpy使いたいが
これを使おう!