![PyO3 基础对象定制指南:为 [pyclass] 实现 repr、str、哈希、比较与布尔语义](http://pic.xiahunao.cn/yaotu/PyO3 基础对象定制指南:为 [pyclass] 实现 repr、str、哈希、比较与布尔语义)
PyO3 基础对象定制指南为 #[pyclass] 实现 repr、str、哈希、比较与布尔语义【免费下载链接】pyo3Rust bindings for the Python interpreter项目地址: https://gitcode.com/gh_mirrors/py/pyo3本篇指南以 PyO3 官方文档《Basic object customization》guide/src/class/object.md为骨架系统讲解如何为 Rust 编写的#[pyclass]类型补齐 Python 基础对象协议字符串表示__repr__/__str__、哈希__hash__、比较运算__richcmp__/__eq__等与真假值__bool__。读完本文你将能够把一个只能被实例化的裸#[pyclass]类打磨成行为与原生 Python 类一致的完整对象并通过 PyO3 的编译期选项str、eq、ord、hash大幅简化样板代码。起点一个只支持实例化的 Number 类回顾前一章PyO3 class 基础定义的Number类它通过#[pyclass]声明为 Python 类并在#[pymethods]中提供#[new]构造器最后经#[pymodule]导出use pyo3::prelude::*; #[pyclass] struct Number(i32); #[pymethods] impl Number { #[new] fn new(value: i32) - Self { Self(value) } } #[pymodule] mod my_module { #[pymodule_export] use super::Number; }此时 Python 代码已经可以导入模块、访问类并创建实例但除此之外什么都做不了from my_module import Number n Number(5) print(n)builtins.Number object at 0x000002B4D185D7D0输出的是 Python 默认的对象表示没有任何可读信息。这正是本篇文章要解决的问题——通过实现 Python 的特殊方法dunder 方法让自定义类具备完整的对象语义。字符串表示repr与str连一个可读的自我表示都打印不出来显然无法接受。修复方式是在#[pymethods]块内定义__repr__和__str__方法并通过访问Number内部的字段来构造字符串#[pymethods] impl Number { // 对于 __repr__我们希望返回一段 Python 代码可以直接用来重建 // Number 的字符串例如 Number(5)。 fn __repr__(self) - String { // format! 宏的第一个参数是格式字符串后面的参数会替换 // 格式串中的 {} 占位符。 // // Rust 中访问元组字段用点号 format!(Number({}), self.0) } // __str__ 通常用于生成非正式的表示这里直接转发给 // i32 的 ToString trait 实现打印一个裸数字。 fn __str__(self) - String { self.0.to_string() } }两个方法的职责分工与 Python 语义完全一致__repr__面向开发者追求无歧义、可重建__str__面向用户追求可读、自然。用 pyclass(str) 自动生成str如果想让__str__直接复用 Rust 的Displaytrait 实现可以给#[pyclass]传str参数PyO3 会自动生成__str__use std::fmt::{Display, Formatter}; use pyo3::prelude::*; #[pyclass(str)] struct Coordinate { x: i32, y: i32, z: i32, } impl Display for Coordinate { fn fmt(self, f: mut Formatter_) - std::fmt::Result { write!(f, ({}, {}, {}), self.x, self.y, self.z) } }这一选项在宏后端的参数解析中被定义为PyClassArgs的str: OptionStrFormatterAttribute字段见 pyo3-macros-backend/src/pyclass.rs#L92既支持普通结构体也支持枚举与复杂字段组合。仓库中的测试 tests/test_class_formatting.rs 演示了#[pyclass(str)]作用于Point2结构体、ComplexEnumWithStr枚举的完整用法#[pyclass(str)] #[derive(PartialEq, Eq, Clone, PartialOrd)] pub struct Point2 { x: i32, y: i32, z: i32, } impl Display for Point2 { fn fmt(self, f: mut Formatter_) - std::fmt::Result { write!(f, ({}, {}, {}), self.x, self.y, self.z) } }便捷的格式字符串简写str 为了进一步减少样板代码str参数还可以直接接收一段格式字符串仅适用于结构体structs。它会被展开并传入format!宏展开规则如下{x}→{}, self.x{0}→{}, self.0{x:?}→{:?}, self.x#[pyclass(str({x}, {y}, {z}))] struct Coordinate { x: i32, y: i32, z: i32, }[!NOTE] 取决于你使用的格式字符串对应的 Rust 类型可能需要实现Display或Debugtrait。pyclass 参数name和rename_all与简写格式字符串不兼容同时使用会触发编译期错误。仓库测试 tests/test_class_formatting.rs 展示了简写格式的多种进阶玩法验证了实际行为按位置引用字段#[pyclass(str {0}, {1}, {2})]作用于元组结构体Coord(u32, u32, u32)str(var1) 1, 2, 3转义花括号#[pyclass(str {{{0}, {1}, {2}}})]输出{1, 2, 3}字段混合与重复引用str name: {name}: {name}, idn: {idn:03} with message: {msg}支持字段多次出现以及{idn:03}这类补零格式raw 标识符字段#[pyclass(str type: {r#type})]可引用 Rust 的r#type字段。这些用例均由测试断言如py_assert!(py, var1, str(var1) X: 1, Y: 2, Z: 3)验证读者可直接运行cargo test -p pyo3 --test test_class_formatting复现。动态获取类名使用 Bound 作为 self在前面的__repr__中我们硬编码了类名字符串Number。这有时并不理想——如果该类在 Python 中被继承我们希望 repr 反映的是子类的名字。Python 中通常通过self.__class__.__name__实现这一点而在 PyO3 中若要同时访问 Python 类型信息和 Rust 结构体字段需要把self参数声明为Bounduse pyo3::prelude::*; use pyo3::types::PyString; #[pymethods] impl Number { fn __repr__(slf: Bound_, Self) - PyResultString { // 等价于 Python 中的 self.__class__.__name__。 let class_name: Bound_, PyString slf.get_type().qualname()?; // 要访问 Rust 结构体字段需要先从 Bound 对象借用。 Ok(format!({}({}), class_name, slf.borrow().0)) } }这里slf.get_type()返回类的PyTypequalname()取得限定名即子类名而slf.borrow()则通过 PyO3 的借用机制获取 Rust 侧的可变/不可变访问。当Number被子类化后repr会自动显示子类名称。哈希hash接下来实现哈希。最简单的方式是对内部的i32进行哈希需要使用std提供的Hashertraitstd自带的DefaultHasher基于 SipHash 算法use std::collections::hash_map::DefaultHasher; // 需要引入 trait 才能调用 .hash 和 .finish 方法。 use std::hash::{Hash, Hasher}; #[pymethods] impl Number { fn __hash__(self) - u64 { let mut hasher DefaultHasher::new(); self.0.hash(mut hasher); hasher.finish() } }编译期选项pyclass(frozen, eq, hash)如果希望直接复用 Rust 的Hashtrait 实现可以使用hash选项。该选项只对frozen类开放目的是防止对象在生命周期内哈希值因可变而改变可变类需要哈希时请使用上面手写__hash__的方式。此外该选项强制要求同时指定eq——依据 Python 数据模型文档 的约定如果一个类没有定义__eq__()它也不应该定义__hash__()操作#[pyclass(frozen, eq, hash)] #[derive(PartialEq, Hash)] struct Number(i32);哈希不变量与关闭哈希[!NOTE] 实现__hash__与比较运算时以下性质必须成立k1 k2 - hash(k1) hash(k2)即两个键相等则它们的哈希值必须相等。此外必须保证类实例的哈希值在生命周期内不变——本教程通过不让 Python 代码修改Number使其不可变来达成这一点。默认情况下所有#[pyclass]类型都会继承 Python 的默认哈希实现。不希望可哈希的类型可以像纯 Python 类一样把__hash__显式置为None#[pyclass] struct NotHashable {} #[pymethods] impl NotHashable { #[classattr] const __hash__: OptionPyPyAny None; }比较运算richcmp与 CompareOpPyO3 支持 Python 中所有常见的比较魔术方法__eq__、__lt__等。更高效的做法是用一个__richcmp__同时覆盖全部六种运算该方法会根据具体操作收到一个CompareOp枚举值use pyo3::class::basic::CompareOp; #[pymethods] impl Number { fn __richcmp__(self, other: Self, op: CompareOp) - PyResultbool { 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), } } }CompareOp定义在 src/pyclass.rs#L33-L94其六个变体Lt/Le/Eq/Ne/Gt/Ge直接映射到 CPython 的 C 枚举常量ffi::Py_LT、ffi::Py_EQ等。源码还提供了from_raw用于从 C 枚举转换以及本文重点用到的matches方法。用 CompareOp::matches 简化如果比较结果来自两个 Rust 值的比较像本例这样可以借助CompareOp::matches一行完成它负责检查 RustOrd得到的std::cmp::Ordering是否与给定的CompareOp匹配use pyo3::class::basic::CompareOp; #[pymethods] impl Number { fn __richcmp__(self, other: Self, op: CompareOp) - bool { op.matches(self.0.cmp(other.0)) } }对照源码 src/pyclass.rs#L84-L93 可以看到其判定逻辑Eq要求Ordering::EqualNe要求非EqualLt要求LessGt要求GreaterLe只要不是GreaterGe只要不是Less。单独实现eq如果只需要相等性比较实现__eq__即可#[pymethods] impl Number { fn __eq__(self, other: Self) - bool { self.0 other.0 } }编译期选项eq 与 ord与str/hash类似PyO3 也提供了基于 Rust trait 自动生成比较方法的选项eq基于 RustPartialEqtrait 实现__eq__#[pyclass(eq)] #[derive(PartialEq)] struct Number(i32);ord基于 RustPartialOrdtrait 实现__lt__、__le__、__gt__与__ge__#[pyclass(eq, ord)] #[derive(PartialEq, PartialOrd)] struct Number(i32);[!NOTE]ord依赖eq必须同时指定。仓库测试 tests/test_class_comparisons.rs 对eq/ord选项做了系统验证可作为行为参考简单枚举#[pyclass(eq)]var1 var2为True、var1 ! other_var为True与字符串foo比较返回Falsetest_enum_eq_incomparable#[pyclass(eq, ord)]的结构体Point { x, y, z }(var1 var2) True、(var3 var2) True、(var4 var5) Truetest_struct_numeric_ord_comparable需要自定义比较逻辑时可手写impl PartialOrd如Record仅按idx比较见test_struct_custom_ord_comparable仅声明eq而未声明ord的枚举执行会抛出PyTypeErrortest_enum_ord_comparable_opt_in_only说明排序比较是opt-in的不会凭空出现。真假值bool将Number定义为非零即为True#[pymethods] impl Number { fn __bool__(self) - bool { self.0 ! 0 } }定义__bool__后if n:、bool(n)、and/or/not等 Python 真假判断都会按此语义执行未定义时 Python 默认对象非 None 即为真。完整示例聚合所有基础协议将以上所有实现整合到同一个类中得到本文的最终代码use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use pyo3::prelude::*; use pyo3::class::basic::CompareOp; use pyo3::types::PyString; #[pyclass] struct Number(i32); #[pymethods] impl Number { #[new] fn new(value: i32) - Self { Self(value) } fn __repr__(slf: Bound_, Self) - PyResultString { let class_name: Bound_, PyString slf.get_type().qualname()?; Ok(format!({}({}), class_name, slf.borrow().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) - PyResultbool { 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] mod my_module { #[pymodule_export] use super::Number; }构建并导入后Number(5)将拥有完整的 Python 对象行为n Number(5) repr(n) # Number(5)若被继承则显示子类名 str(n) # 5 hash(n) # 基于内部 i32 的 SipHash 值 n Number(3) # True n Number(5) # True bool(Number(0)) # False小结手写与编译期选项如何取舍回顾整篇文章PyO3 为#[pyclass]的基础对象定制提供了两套互补的机制可在 pyo3-macros-backend/src/pyclass.rs#L50-L114 的PyClassArgs中看到完整的参数清单Python 协议手写方式#[pymethods] 内编译期选项#[pyclass(...)]__repr__fn __repr__(self) - String推荐用Bound获取类名—str选项只生成__str____str__fn __str__(self) - Stringstr复用Displaystrfmt结构体简写__hash__fn __hash__(self) - u64可变类必须手写frozen, eq, hash复用Hash/PartialEq__eq__fn __eq__(self, other: Self) - booleq复用PartialEq__lt__/__le__/__gt__/__ge____richcmp__(self, other, op: CompareOp)eq, ord复用PartialOrd关闭哈希#[classattr] const __hash__: OptionPyPyAny None—__bool__fn __bool__(self) - bool—选择建议数据语义简单、字段布局稳定的值类型如坐标、数值包装优先使用编译期选项代码量最少且与 Rust 标准 trait 保持一致性需要动态类名、自定义哈希策略或复杂比较逻辑时再手写特殊方法。若要验证以上行为可参考仓库中的 tests/test_class_formatting.rs 与 tests/test_class_comparisons.rs 测试用例运行cargo test -p pyo3 --test test_class_formatting与cargo test -p pyo3 --test test_class_comparisons复现全部断言。【免费下载链接】pyo3Rust bindings for the Python interpreter项目地址: https://gitcode.com/gh_mirrors/py/pyo3创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考