LoginSignup
0
0

PythonのコードをRustから使う その6 Python関数の引数にserde_json::Valueを渡す

Posted at

前の記事

概要

関数の返り値は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)}
0
0
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
0
0