自定义基本对象

回忆上一章中的Number

#![allow(unused)]
fn main() {
use pyo3::prelude::*;

#[pyclass]
struct Number(i32);

#[pymethods]
impl Number {
    #[new]
    fn new(value: i32) -> Self {
        Self(value)
    }
}

#[pymodule]
fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
    m.add_class::<Number>()?;
    Ok(())
}
}

目前Python可以导入这个模组,获取到这个类,并创建实例——没别的了。

from my_module import Number

n = Number(5)
print(n)
<builtins.Number object at 0x000002B4D185D7D0>

字符串表示String representations

它甚至不能打印一个用户可见的它自身的说明!我们可以在#[pymethods]块中加上__repr____str__ 方法,用这个来获得Number中包含的值。methods inside a

#![allow(unused)]
fn main() {
use pyo3::prelude::*;

#[pyclass]
struct Number(i32);

#[pymethods]
impl Number {
    // For `__repr__` we want to return a string that Python code could use to recreate
    // the `Number`, like `Number(5)` for example.
    fn __repr__(&self) -> String {
        // We use the `format!` macro to create a string. Its first argument is a
        // format string, followed by any number of parameters which replace the
        // `{}`'s in the format string.
        //
        //                       👇 Tuple field access in Rust uses a dot
        format!("Number({})", self.0)
    }

    // `__str__` is generally used to create an "informal" representation, so we
    // just forward to `i32`'s `ToString` trait implementation to print a bare number.
    fn __str__(&self) -> String {
        self.0.to_string()
    }
}
}

哈希Hashing

接下来实现哈希。我们仅仅哈希i32,为此我们需要一个Hashstd提供的是DefaultHasher,使用的是SipHash算法。

#![allow(unused)]
fn main() {
use std::collections::hash_map::DefaultHasher;

// Required to call the `.hash` and `.finish` methods, which are defined on traits.
use std::hash::{Hash, Hasher};

use pyo3::prelude::*;

#[pyclass]
struct Number(i32);

#[pymethods]
impl Number {
    fn __hash__(&self) -> u64 {
        let mut hasher = DefaultHasher::new();
        self.0.hash(&mut hasher);
        hasher.finish()
    }
}
}

Note: 当实现 __hash__ 和比较时, 下属性质需要满足

k1 == k2 -> hash(k1) == hash(k2)

也就是说两个键值相等时,它们的哈希也要相等。 另外必须注意类的哈希不能在生命周期中改变。在这里的教程中,我们不让Python代码改变我们的Number类,也就是说,它是不可变的。 默认情况下,所有 #[pyclass] 类型在Python中有一个默认的哈希实现。 不需要被哈希的类型需要改写__hash__None

#![allow(unused)]
fn main() {
use pyo3::prelude::*;
#[pyclass]
struct NotHashable {}

#[pymethods]
impl NotHashable {
    #[classattr]
    const __hash__: Option<Py<PyAny>> = None;
}
}

比较Comparisons

不像在Python中,PyO3不提供类似__eq__, __lt__的魔术比较方法。你需要通过__richcmp__一次性实现所有六个算子。 Unlike in Python, PyO3 does not provide the magic comparison methods you might expect like __eq__, 这个方法会根据算子的CompareOp值被调用

#![allow(unused)]
fn main() {
use pyo3::class::basic::CompareOp;

use pyo3::prelude::*;

#[pyclass]
struct Number(i32);

#[pymethods]
impl Number {
    fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult<bool> {
        match op {
            CompareOp::Lt => Ok(self.0 < other.0),
            CompareOp::Le => Ok(self.0 <= other.0),
            CompareOp::Eq => Ok(self.0 == other.0),
            CompareOp::Ne => Ok(self.0 != other.0),
            CompareOp::Gt => Ok(self.0 > other.0),
            CompareOp::Ge => Ok(self.0 >= other.0),
        }
    }
}
}

如果通过比较两个Rust值获得了结果,可以采取一个捷径CompareOp::matches

#![allow(unused)]
fn main() {
use pyo3::class::basic::CompareOp;

use pyo3::prelude::*;

#[pyclass]
struct Number(i32);

#[pymethods]
impl Number {
    fn __richcmp__(&self, other: &Self, op: CompareOp) -> bool {
        op.matches(self.0.cmp(&other.0))
    }
}
}

它会检查Rust的 Ord 中得到的 std::cmp::Ordering 是否匹配给定的CompareOp

另外,如果想要保留一些算子不被实现,可以返回py.NotImplemented()

#![allow(unused)]
fn main() {
use pyo3::class::basic::CompareOp;

use pyo3::prelude::*;

#[pyclass]
struct Number(i32);

#[pymethods]
impl Number {
    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> PyObject {
        match op {
            CompareOp::Eq => (self.0 == other.0).into_py(py),
            CompareOp::Ne => (self.0 != other.0).into_py(py),
            _ => py.NotImplemented(),
        }
    }
}
}

真伪Truthyness

考虑NumberTrue如果它不是零

#![allow(unused)]
fn main() {
use pyo3::prelude::*;

#[pyclass]
struct Number(i32);

#[pymethods]
impl Number {
    fn __bool__(&self) -> bool {
        self.0 != 0
    }
}
}

最终的代码

#![allow(unused)]
fn main() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

use pyo3::prelude::*;
use pyo3::class::basic::CompareOp;

#[pyclass]
struct Number(i32);

#[pymethods]
impl Number {
    #[new]
    fn new(value: i32) -> Self {
        Self(value)
    }

    fn __repr__(&self) -> String {
        format!("Number({})", self.0)
    }

    fn __str__(&self) -> String {
        self.0.to_string()
    }

    fn __hash__(&self) -> u64 {
        let mut hasher = DefaultHasher::new();
        self.0.hash(&mut hasher);
        hasher.finish()
    }

    fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult<bool> {
        match op {
            CompareOp::Lt => Ok(self.0 < other.0),
            CompareOp::Le => Ok(self.0 <= other.0),
            CompareOp::Eq => Ok(self.0 == other.0),
            CompareOp::Ne => Ok(self.0 != other.0),
            CompareOp::Gt => Ok(self.0 > other.0),
            CompareOp::Ge => Ok(self.0 >= other.0),
        }
    }

    fn __bool__(&self) -> bool {
        self.0 != 0
    }
}

#[pymodule]
fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
    m.add_class::<Number>()?;
    Ok(())
}
}