0
0

PythonのコードをRustから使う その4 Pythonのディクショナリとserde_json::Valueを変換

Last updated at Posted at 2024-06-12

前回の記事

概要

Pythonからディクショナリを受け取るようになるとコードが整理されて便利。ちょうどserde_jsonと合わせるのが便利なのでメモしとく。

やはりwith_gilの中でPyDict型をserde_json::Valueに変換するのだけど、型を確認しながら呼び出すコードを書いたのでシェアする。未対応のタイプがあるので注意してね。あと、エラー処理省略してるので注意。

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"]}

src/main.rs

use std::collections::HashMap;

use pyo3::{prelude::*, types::{PyDict, PyFloat, PyInt, PyList, PyNone, PyString, PyTuple}};
use serde_json::{json, Value}; // 主要なモジュール

fn main() {
    let mut rust_value: Value = json!({});
    let arg0 = 1;
    let arg1 = 2;

    // Python::with_gilでPython環境呼び出す
    let pyresult  = Python::with_gil(|py| -> PyResult<Py<PyAny>> {
        // ここでコードをモジュールとしてロード。ロードした時にグローバルに置いたprintの行が実行される
        let module = PyModule::from_code_bound(py, 
            r#" 
print("Hello, World from Python!")

def function1(arg0, arg1):
    print("function1() called. add two args.")
    return {
        "sub": arg0 - arg1,
        "add": arg0 + arg1,
        "str": f"arg0 is '{arg0}', arg1 is '{arg1}'",
        }
            "#
            , "", "")?;
        // ここでモジュールの中のfunction1にアクセスできるようになる
        let app_func: Py<PyAny> = module.getattr("function1")?.into();
        // 引数をPyTupleでまとめて呼び出し。返却値を受け取る。
        let func_result = app_func.call1(py, PyTuple::new_bound(py, [arg0, arg1]))?;

        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))
}

cargo new

$ cargo new
Hello, World from Python!
function1() called. add two args.
pyresult is Ok(Py(0x100a6b6c0))
rust_value is Object {"add": Number(3), "str": String("arg0 is '1', arg1 is '2'"), "sub": Number(-1)}

一部、"一度JSONに変換して渡せばいいじゃない"という投稿があって、気に入らないから作った。

次回の記事

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