pyo3でRust側のエラーをPython側でキャッチする

今回は，[pyo3](https://github.com/PyO3/pyo3)クレートを用いてPythonからRustで定義した処理を呼び出すときのエラーハンドリングをしてみます．具体的にはRust側で[thiserror](https://docs.rs/thiserror/latest/thiserror/)を用いて定義したエラーをPython側でキャッチします．

簡単なものですが，プログラムは[こちら](https://github.com/deepgreenAN/pyo3_error_handling)にあります．

## エラーの定義
エラーは以下で定義しています．
列挙体はRust側でエラーハンドリングをするために作成しているので，Python側でのみエラーを扱うなら必要ありません．

```rust:error.rs
#[derive(thiserror::Error, Debug)]
pub enum MathError {
    #[error(transparent)]
    LogSmallError(#[from] LogSmallError),

    #[error(transparent)]
    ExpLargeError(#[from] ExpLargeError)
}

#[derive(thiserror::Error, Debug)]
#[error("too small x for log(x) x:{0}. (expected x > 0.01)")]
pub struct LogSmallError(pub f64);

#[derive(thiserror::Error, Debug)]
#[error("too large x for exp(x) x:{0}. (expected x < 10)")]
pub struct ExpLargeError(pub f64);

#[derive(thiserror::Error, Debug)]
#[error("too small abs y for x / y y:{0}. (expected y > 0.001)")]
pub struct DivSmallAbsError(pub f64);

```

## RustのエラーをPythonからimportする
pyo3の[ユーザーガイド](https://pyo3.rs/v0.13.2/exception.html)にあるように，まず`create_exception!`マクロでPythonから利用できるExceptionを定義します．第一引数はこの`lib.rs`で定義するモジュール名，第二引数はException名，第三引数は継承するスーパークラスです．ここでは簡単のために`error.rs`で定義したエラーと同じ名前としているので，`lib.rs`から`error.rs`の同名のエラーを利用する場合は`crate::error::MathError`のようにします．`DivSmallAbsError`については`ValueError`としてraiseさせるので，ここでは定義しません．

```rust:lib.rs
use pyo3::create_exception;
create_exception!(error_handling, MathError, pyo3::exceptions::PyException);

create_exception!(error_handling, LogSmallError, MathError);
create_exception!(error_handling, ExpLargeError, MathError);
```

次に各エラーから`PyErr`に変換できるようにFromトレイトを実装します．Fromトレイトを実装することで，`pyo3::PyResult<T>`（`Rsult<T, PyErr>`のエイリアス）に`?`で返せるようになります．`DivSmallAbsError`は`ValueError`としてraiseするために`pyo3::exceptions::PyValueError`から作成しています．

```rust:lib.rs
impl From<crate::error::MathError> for PyErr {
    fn from(err: crate::error::MathError) -> PyErr {
        MathError::new_err(err.to_string())
    }
}

impl From<crate::error::LogSmallError> for PyErr {
    fn from(err: crate::error::LogSmallError) -> PyErr {
        LogSmallError::new_err(err.to_string())
    }
}


impl From<crate::error::ExpLargeError> for PyErr {
    fn from(err: crate::error::ExpLargeError) -> PyErr {
        ExpLargeError::new_err(err.to_string())
    }
}

impl From<crate::error::DivSmallAbsError> for PyErr {
    fn from(err: crate::error::DivSmallAbsError) -> PyErr {
        pyo3::exceptions::PyValueError::new_err(err.to_string())
    }
}
```

最後に，定義したExceptionをPythonから呼べるように登録します．これでエラーをExceptionとしてPythonからimportできるようになります．

```rust:lib.rs
#[pymodule]
fn error_handling(py: Python, m:&PyModule) -> PyResult<()> {
    m.add("MathError", py.get_type::<MathError>())?;
    m.add("LogSmallError", py.get_type::<LogSmallError>())?;
    m.add("ExpLargeError", py.get_type::<ExpLargeError>())?;


    ...その他関数の登録
}
```

## Rust側でエラーを返しPythonでキャッチする

Fromトレイトを実装しているのでエラーを`?`で返せます．`PyResult<T>`の代わりに`Result<T, crate::error::MathError>`を返り値の型とすることもできますが，Python側で`MathError`としてraiseされてしまいます．

```rust:lib.rs
fn log(x: f64) -> Result<f64, crate::error::LogSmallError> {
    if x < 0.01 {
        return Err(crate::error::LogSmallError(x));
    } else {
        return Ok(x.ln());
    }
}

fn exp(x: f64) -> Result<f64, crate::error::ExpLargeError> {
    if x > 10.0 {
        return Err(crate::error::ExpLargeError(x));
    } else {
        return Ok(x.exp());
    }
}

#[pyfunction]
fn rust_log(x: f64) -> PyResult<f64> {
    Ok(log(x)?)
}

#[pyfunction]
fn rust_exp(x: f64) -> PyResult<f64> {
    Ok(exp(x)?)
}
```
以上の関数をPython(jupyter)から呼んで例外を起こすと以下のようになります．

```python:example.ipynb
from error_handling import MathError, LogSmallError, ExpLargeError
from error_handling import rust_log, rust_exp, rust_div, rust_log_with_exp

rust_log(0.0)
```

    ---------------------------------------------------------------------------

    LogSmallError                             Traceback (most recent call last)

    C:\Users\-\AppData\Local\Temp/ipykernel_10728/2422354142.py in <module>
    ----> 1 rust_log(0.0)
    

    LogSmallError: too small x for log(x) x:0. (expected x > 0.01)


```python:example.ipynb
try:
    rust_exp(1000)
except ExpLargeError as err:
    print(err)
```

    too large x for exp(x) x:1000. (expected x < 10)


もちろん，スーパークラスを指定してもサブクラスをキャッチできます．

```python:example.ipynb
try:
    rust_exp(1000)
except MathError as err:
    print(err)
```

    too large x for exp(x) x:1000. (expected x < 10)

Fromトレイトの実装で`PyValueError`から作成した`DivSmallAbsError`はPythonでは`ValueError`がraiseされます．

```rust:lib.rs
fn div(x: f64, y: f64) -> Result<f64, crate::error::DivSmallAbsError> {
    if y < 0.001 {
        return Err(crate::error::DivSmallAbsError(y));
    } else {
        return Ok(x / y);
    }
}

#[pyfunction]
fn rust_div(x: f64, y: f64) -> PyResult<f64> {
    Ok(div(x, y)?)
}
```

```python:example.ipynb
rust_div(1.0, 0.0)
```

    ---------------------------------------------------------------------------

    ValueError                                Traceback (most recent call last)

    C:\Users\-\AppData\Local\Temp/ipykernel_10728/109446888.py in <module>
    ----> 1 rust_div(1.0, 0.0)
    

    ValueError: too small abs y for x / y y:0. (expected y > 0.001)

以上のようにpyo3ではRust側でも，Pythonで利用できるエラーが定義できます．さらに
Python3.10ではmatch式が導入されたので，rustでエラーコードを返すような以下の処理を

```rust:lib.rs
fn log_with_exp(x: f64) -> Result<(f64, f64), crate::error::MathError> {
    let log_x = log(x)?;
    let exp_x = exp(x)?;
    Ok((log_x, exp_x))
}

fn rust_handle_error(x: f64) -> i64 {
    let res = log_with_exp(x);
    if let Err(error) = res {
        match error {
            crate::error::MathError::LogSmallError(_) => { return 1;},
            crate::error::MathError::ExpLargeError(_) => { return 2;}
        }
    } else {
        return 0;
    }
}
```

match式を用いて以下のように簡単に実装できます．

```python:example.ipynb
def handle_error(x: float) -> int:
    try:
        rust_log(x)
        rust_exp(x)
    except MathError as err:
        match err:
            case LogSmallError():
                return 1
            case ExpLargeError():
                return 2
            case _:
                return 100
    else:
        return 0
```