前の記事
概要
関数の返り値はPyDictからserde_json::Valueに変換してたけど、その逆をメモしてなかったから買いとく。
ファイルツリー
$ tree
.
├── Cargo.toml
├── scripts
│ ├── mymodule.py
│ └── othermodule
│ └── __init__.py
└── src
└── main.rs
Cargo.toml
[package]
name = "pyo3_test"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = {version = "1.0.193", features = ["derive"]}
serde_json="1.0.108"
pyo3 = {version = "*", features=["auto-initialize"]}
scripts/mymodule.py
from othermodule import other_module_function
def function1(arg0, arg1, arg_dict):
print("function1() called. add two args.")
return {
"sub": arg0 - arg1,
"add": arg0 + arg1,
"str": f"arg0 is '{arg0}', arg1 is '{arg1}'",
"othermodule": other_module_function(),
"arg_dict": arg_dict
}
scripts/othermodule/init.py
def other_module_function():
return "This is other_module_function result."
src/main.rs
use std::{collections::HashMap, fs, path::PathBuf};
use pyo3::{prelude::*, types::{PyDict, PyFloat, PyInt, PyList, PyNone, PyString, PyTuple}};
use serde_json::{json, Map, Value}; // 主要なモジュール
fn main() {
let mut rust_value: Value = json!({});
let arg0 = 1;
let arg1 = 2;
let arg_dict = json!({"foo": "bar", "data": 1, "value": 2});
// ロードするPythonファイルのフルパス
let path_to_pyfile = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("scripts/mymodule.py");
// Python::with_gilでPython環境呼び出す
let pyresult: Result<Py<PyAny>, PyErr> = Python::with_gil(|py| -> PyResult<Py<PyAny>> {
let parent = path_to_pyfile.parent().unwrap().to_str().unwrap(); // 指定されたPythonファイルがあるディレクトリのパスを取得
// ここでsys.pathに取得したparentの値を追加すると、ここからimportできる。
let _ = PyModule::from_code_bound(py, &format!(r#"
import sys
if not "{parent:}" in sys.path:
sys.path.append("{parent:}")
"#), "", "");
// コードを文字列に変換
let app_code = fs::read_to_string(path_to_pyfile)?;
// ここでコードをモジュールとしてロード。ロードした時にグローバルに置いたprintの行が実行される
let module = PyModule::from_code_bound(py,
app_code.as_str(), "", "")?;
// ここでモジュールの中のfunction1にアクセスできるようになる
let app_func: Py<PyAny> = module.getattr("function1")?.into();
// 引数をPyTupleでまとめて呼び出し。返却値を受け取る。
// 引数の型が違うので、単純配列じゃなくてPy<PyAny>型に変換してから配列にしてる。
// Py<PyAny>への変換はinto_pyを使う
// value_to_pyany関数は下の方で定義している、serde_json::ValueをPyAnyに変換する関数
let func_result = app_func.call1(py, PyTuple::new_bound(py, [arg0.into_py(py), arg1.into_py(py), value_to_pyany(py, &arg_dict)]))?;
let ref_pydict = func_result.clone().extract::<&PyDict>(py)?; // 結果をPyDictの参照に変換する
rust_value = pydict_to_value(ref_pydict)?; // この関数でPyDictをserde_json::Valueに変換する。
Ok(func_result)
});
println!("pyresult is {pyresult:?}");
println!("rust_value is {rust_value:?}");
}
/// 個々のPyAny型変数の型を確認しながら変換する
pub fn pyany_to_value(value: &PyAny) -> PyResult<Value> {
if value.is_instance_of::<PyString>() {
Ok(Value::from(value.extract::<String>()?))
} else if value.is_instance_of::<PyFloat>() {
Ok(Value::from(value.extract::<f64>()?))
} else if value.is_instance_of::<PyInt>() {
Ok(Value::from(value.extract::<i64>()?))
} else if value.is_instance_of::<PyList>() {
pylist_to_value(value.extract::<&PyList>()?)
} else if value.is_instance_of::<PyDict>() {
pydict_to_value(value.extract::<&PyDict>()?)
} else if value.is_instance_of::<PyNone>() {
Ok(Value::Null)
} else {
Err(pyo3::exceptions::PyException::new_err("未対応のタイプなので失敗"))
}
}
/// リストをValueに変換
fn pylist_to_value(pylist: &PyList) -> PyResult<Value> {
let mut vec: Vec<Value> = Vec::new();
for value in pylist.into_iter() {// 中身を一個ずつPyAnyからValueに変換する
vec.push(pyany_to_value(value)?);
}
Ok(vec.into())
}
/// PyDictをValueに変換
pub fn pydict_to_value(pydict: &PyDict) -> PyResult<Value> {
let mut map: HashMap<String, Value> = HashMap::new();
for (key, value) in pydict.into_iter() { // 中身を一個ずつPyAnyからValueに変換する
map.insert(key.extract::<String>()?, pyany_to_value(value)?);
}
Ok(json!(map))
}
/// serde_jsonで定義される配列をPyAnyに変換
fn valuearray_to_pyany(py: Python, arr: &Vec<Value>) -> Py<PyAny> {
arr.iter().map(|v| { value_to_pyany(py, v) }).collect::<Vec<Py<PyAny>>>().into_py(py)
}
/// serde_jsonで定義されるMapをPyAnyに変換
fn valueobj_to_pyany(py: Python, map: &Map<String, Value>) -> Py<PyAny> {
map.iter().map(|(k, v)| { (k.clone(), value_to_pyany(py, v)) }).collect::<HashMap<String, Py<PyAny>>>().into_py(py)
}
/// serde_json::ValueをPyAnyに変換
fn value_to_pyany(py: Python, value: &Value) -> Py<PyAny> {
if value.is_i64() {
return (value.as_i64().unwrap()).into_py(py);
} else if value.is_f64() {
return (value.as_f64().unwrap()).into_py(py);
} else if value.is_boolean() {
return (value.as_bool().unwrap()).into_py(py);
} else if value.is_string() {
return (value.as_str().unwrap()).into_py(py);
} else if value.is_null() {
return (value.as_null().unwrap()).into_py(py);
} else if value.is_u64() {
return (value.as_u64().unwrap()).into_py(py);
} else if value.is_array() {
return valuearray_to_pyany(py, value.as_array().unwrap());
} else if value.is_object() {
return valueobj_to_pyany(py, value.as_object().unwrap());
}
todo!()
}
cargo run
$ cargo run
function1() called. add two args.
pyresult is Ok(Py(0x102c43b40))
rust_value is Object {"add": Number(3), "arg_dict": Object {"data": Number(1), "foo": String("bar"), "value": Number(2)}, "othermodule": String("This is other_module_function result."), "str": String("arg0 is '1', arg1 is '2'"), "sub": Number(-1)}