Overview

PyO3 是一个用于 rust 和 python 代码互操作的库,可以用来给 python 代码写加速库,提升Python的运行速度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use pyo3::prelude::*;

#[pyfunction]
pub fn double(x: usize) -> usize {
x * 2
}

#[pyclass]
struct Number(i32);

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

/// 这是一个文档
#[pymodule]
#[pyo3(name = "add_as_string")]
fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(double, m)?)?;
m.add_class::<Number>()?;
Ok(())
}

然后在Cargo.toml中添加以下内容:

1
2
3
[lib]
name = "my_module"
crate-type = ["cdylib"]

然后将整个rust crate编译成动态链接库,就可以在Python 代码中引入。在 windows 平台上整个模块会编译成 DLL 文件,我们需要手动将文件后缀名改为 .pyd; 而在 Mac 平台和 Linux 平台上,这个crate会编译成 .so文件,我们可以在python中直接引入这个文件。

PyO3官方给我们提供了手脚架 maturin,这个手脚架能帮我们自动处理跨平台的兼容性。我们可以简单使用命令pip install maturin安装这个手脚架。然后使用 maturin init在当前目录下初始化一个新PyO3的项目。(如果安装了uv的话则可以跳过安装步骤,直接使用uvx maturin init

在开发的时候,我们可以将使用maturin develop直接将包安装到当前目录下的Python虚拟环境中,用于实时修改和 debug。

1
2
3
4
5
6
7
your_project/
├── Cargo.toml
├── src/
│ └── lib.rs
├── pyproject.toml
└── python/
└── example.py # 可选:存放测试用的 Python 脚本

当我们编写完整个库后,就可以使用maturin build --release命令构建出.whl文件,最后在Python项目中使用pip安装编译出的.whl文件即可。

#[pymodule]

<font style="color:rgb(0, 0, 0);">#[pymodule]</font> 过程宏负责将模块的初始化函数导出到 Python,模块名称默认使用 Rust 函数的名称。你可以通过 <font style="color:rgb(0, 0, 0);">#[pyo3(name = "custom_name")]</font> 来覆盖模块名称,模块名称必须与 .so 或 .pyd 文件的名称匹配。否则在 Python 中导入时会报错。模块初始化函数的 Rust 文档注释将自动作为 Python 模块的文档字符串应用。

或者可以使用以下方法声明一个Python模块以及其成员/嵌套模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
use pyo3::prelude::*;

#[pyfunction]
fn double(x: usize) -> usize {
x * 2
}

#[pymodule]
mod my_extension {
use super::*;

#[pymodule_export]
use super::double; // Exports the double function as part of the module

#[pymodule_export]
const PI: f64 = std::f64::consts::PI; // Exports PI constant as part of the module

#[pyfunction] // This will be part of the module
fn triple(x: usize) -> usize {
x * 3
}

#[pyclass] // This will be part of the module
struct Unit;

#[pymodule]
mod submodule {
// This is a submodule
}

#[pymodule_init]
fn init(m: &Bound<'_, PyModule>) -> PyResult<()> {
// Arbitrary code to run at the module initialization
m.add("double2", m.getattr("double")?)
}
}

#[pyfunction]

<font style="color:rgb(0, 0, 0);">#[pyfunction]</font> 属性用于从 Rust 函数定义 Python 函数。定义完成后,需要使用 <font style="color:rgb(0, 0, 0);">wrap_pyfunction! </font>宏将该函数添加到模块中。<font style="color:rgb(0, 0, 0);">#[pyo3] </font>属性可用于修改生成的 Python 函数的属性。它可以接受以下任意组合的选项:

  • <font style="color:rgb(0, 0, 0);">#[pyo3(name = "...")]</font>覆盖暴露给 Python 的名称。
  • <font style="color:rgb(0, 0, 0);">#[pyo3(signature = "...")] </font>定义函数签名。
  • <font style="color:rgb(0, 0, 0);">#[pyo3(text_signature = "...")]</font>设置函数签名在 Python 工具中的可见性(例如通过<font style="color:rgb(0, 0, 0);">help()</font>显示)。
  • <font style="color:rgb(0, 0, 0);">#[pyo3(pass_module)]</font>设置此选项可使 PyO3 将包含模块作为第一个参数传递给函数,这样便可在函数体内使用该模块。第一个参数必须是 <font style="color:rgb(0, 0, 0);">&Bound<'_, PyModule></font><font style="color:rgb(0, 0, 0);">Bound<'_, PyModule></font><font style="color:rgb(0, 0, 0);">Py<PyModule></font> 类型。

#[pyo3(signature = (…))]

我们可以同过#[pyo3(signature = (**kwds))]用python的方式定义函数的参数类型。可以是以下结构:

  • / :仅限位置参数分隔符,每个在 / 之前定义的参数都是仅限位置参数
  • * : 可变参数分隔符, * 之后定义的每个参数都是仅限关键字参数
  • *args : “args” 表示可变参数。 args 参数的类型必须是&Bound<'_, PyTuple>
  • **kwargs : “kwargs” 接收关键字参数。 kwargs 参数的类型必须是 Option<&Bound<'_, PyDict>>
  • arg=Value : 带默认值的参数。如果 arg 参数定义在可变参数之后,则被视为仅限关键字参数。注意 Value 必须是有效的 Rust 代码,PyO3 会直接将其原样插入生成的代码中

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use pyo3::types::{PyDict, PyTuple};
#[pymethods]
impl MyClass {
#[new]
#[pyo3(signature = (num=-1))]
fn new(num: i32) -> Self {
MyClass { num }
}

#[pyo3(signature = (num=10, *py_args, name="Hello", **py_kwargs))]
fn method(
&mut self,
num: i32,
py_args: &Bound<'_, PyTuple>,
name: &str,
py_kwargs: Option<&Bound<'_, PyDict>>,
) -> String {
let num_before = self.num;
self.num = num;
format!(
"num={} (was previously={}), py_args={:?}, name={}, py_kwargs={:?} ",
num, num_before, py_args, name, py_kwargs,
)
}
}

#[pyo3(from_py_with = “…”)]

#[pyo3(from_py_with = "...")] 属性可用于单个参数,以修改生成函数中这些参数的属性。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use pyo3::prelude::*;

// 将输入的 Python 对象转换为其长度
fn get_length(obj: &PyAny) -> PyResult<usize> {
let length = obj.len()?;
Ok(length)
}

#[pyfunction]
fn object_length(
#[pyo3(from_py_with = "get_length")] argument: usize
) -> usize {
argument
}

PyResult

  1. 任何可能返回Python 错误的的API函数都应该返回PyResult<T>PyResult<T>是类型Result<T, PyErr>的别名,PyErr则表示所有可能的Python错误。
  2. 所有内置 Python 异常类型都定义在 pyo3::exceptions 模块中。它们具有 new_err 构造函数,可直接构建 PyErr ,例如:
1
2
3
4
5
6
7
8
9
10
11
12
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;

#[pyfunction]
fn check_positive(x: i32) -> PyResult<()> {
if x < 0 {
Err(PyValueError::new_err("x is negative"))
} else {
Ok(())
}
}

  1. 所有实现的std::from::From<E> for PyErr的结构体都可以作为自定义的PyErr错误返回。

#[pyclass]

主要属性是 #[pyclass] ,它被放置在 Rust 的 struct 或 enum 上,为其生成 Python 类型。这些结构体通常还会有一个用 #[pymethods] 注解的 impl 代码块,用于为生成的 Python 类型定义方法和常量。(如果启用了 multiple-pymethods 功能,每个 #[pyclass] 可以拥有多个 #[pymethods] 代码块。) #[pymethods] 还可以实现 Python 魔术方法,例如 str

为了将 Rust 类型与 Python 集成,PyO3 需要对能用 #[pyclass] 注解的类型施加一些限制。具体来说,这些类型不能有生命周期参数、不能有泛型参数,且必须是线程安全的。下文将逐一解释这些限制的原因。

:::info

为什么不能有生命周期?

一旦 Rust 数据暴露给 Python,就无法保证 Rust 编译器能确定这些数据的存活时间。Python 是引用计数语言,这些引用可能被持有任意长的时间,而 Rust 编译器无法追踪。唯一正确的表达方式是要求任何 #[pyclass] 不能借用生命周期短于 'static 的数据,也就是说 #[pyclass] 不能有任何生命周期参数。

当需要在 Python 和 Rust 之间共享数据所有权时,不要使用带生命周期的借用引用,而应考虑使用引用计数的智能指针,如 ArcPy

为什么需要是线程安全的?

Python 解释器可以自由在线程间共享 Python 对象。这意味着:

  • Python 对象可能由不同的 Python 线程创建和销毁;因此 #[pyclass] 对象必须是 Send 的。
  • Python 对象可能被多个 Python 线程同时访问;因此 #[pyclass] 对象必须是 Sync 的。

:::

#[pyclass] 可与以下参数配合使用:

Parameter 参数 Description 描述
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">constructor</font> 目前仅允许在复杂枚举的变体上使用。它允许为每个变体自定义生成的类构造函数,使用与函数和方法 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">signature</font>属性相同的语法并支持相同的选项。
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">crate = "some::path"</font> 如果 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">pyo3</font>crate 无法通过 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">::pyo3</font>访问,则指定其导入路径。
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">dict</font> 为此类的实例提供一个空的 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">__dict__</font>用于存储自定义属性。
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">eq</font> 使用底层 Rust 数据类型的 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">PartialEq</font> 实现来实现 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">__eq__</font>
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">eq_int</font> 对于简单枚举类型,使用 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">__int__</font>实现 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">__eq__</font>
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">extends = BaseType</font> 使用自定义基类。默认为 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">PyAny</font>
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">freelist = N</font> 实现大小为 N 的自由列表。对于频繁创建和删除的类型,这可以提高性能。通过性能分析判断 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">freelist</font>是否适合您的场景。
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">frozen</font> 声明您的 pyclass 是不可变的。这消除了获取 Rust 结构体共享引用时的借用检查器开销,但禁用了获取可变引用的能力。
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">generic</font> 为类实现遵循 PEP 560 的运行时参数化功能。
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">get_all</font> 为 pyclass 的所有字段生成 getter 方法。
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">hash</font> 使用底层 Rust 数据类型的 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">Hash</font>实现来实现 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">__hash__</font>功能。
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">immutable_type</font> 使类型对象不可变。在 3.14+版本中需要启用 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">abi3</font>特性才支持,否则需 3.10+版本。
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">mapping</font> 告知 PyO3 这个类是一个 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">Mapping</font>,因此将其序列 C-API 槽位的实现留空。
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">module = "module_name"</font> Python 代码会认为该类是在此模块中定义的。默认为 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">builtins</font>
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">name = "python_name"</font> 设置 Python 中看到的类名称。默认为 Rust 结构体的名称。
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">ord</font> 使用底层 Rust 数据类型的 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">PartialOrd</font>实现来实现 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">__lt__</font><font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">__gt__</font><font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">__le__</font><font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">__ge__</font> 。需要 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">eq</font>
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">rename_all = "renaming_rule"</font> 将重命名规则应用于结构体的所有 getter 和 setter,或枚举的所有变体。可能的值为:
+ “camelCase”、
+ “kebab-case”、
+ “lowercase”、
+ “PascalCase”、
+ “SCREAMING-KEBAB-CASE”、
+ “SCREAMING_SNAKE_CASE”、
+ “snake_case”、
+ “UPPERCASE”。
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">sequence</font> 通知 PyO3 这个类是一个 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">Sequence</font>,因此保持其 C-API 映射长度槽为空。
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">set_all</font> 为 pyclass 的所有字段生成 setter 方法。
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">str</font> 使用底层 Rust 数据类型的 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">Display</font>实现或通过传递可选的格式字符串 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">str="<format string>"</font>来实现 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">__str__</font>。注意:可选格式字符串仅适用于结构体。 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">name</font><font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">rename_all</font>与可选格式字符串不兼容。更多细节可在此 PR 讨论中找到。
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">subclass</font> 允许其他 Python 类和 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">#[pyclass]</font>继承此类。枚举类型不可被子类化。
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">unsendable</font> 当您的结构体不是 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">Send</font>时必须使用。与其使用 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">unsendable</font>,不如通过例如用 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">Arc</font>替换 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">Rc</font>
的方式实现线程安全的结构体。使用 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">unsendable</font>时,当其他线程访问您的类会导致 panic。另请注意 Python 的 GC 是多线程的,虽然不可发送的类不会在外部线程上遍历以避免未定义行为,但这可能导致内存泄漏。
<font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">weakref</font> 允许此类被弱引用。

#[new]

要声明构造函数,需要定义一个方法并用 <font style="color:rgb(0, 0, 0);">#[new]</font> 属性进行标注。若未声明任何标记为 #[new] 的方法,对象实例只能从 Rust 创建,而无法通过 Python 创建。

对于可能失败的构造函数,你也应该将返回类型包装在 PyResult 中。

#[pyo3(get, set)]

  1. 对于没有副作用的简单结构体字段,可以直接在 #[pyclass] 的字段定义上添加 #[pyo3(get, set)] 属性。若要以不同于字段的名称公开该属性,可在其他选项中指定,例如#[pyo3(get, set, name = "custom_name")]
    1. 对于 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">get</font> ,字段类型必须同时实现 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">IntoPy<PyObject></font><font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">Clone</font>
    2. 对于 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">set</font> ,字段类型必须实现 <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">FromPyObject</font>
  2. 对于需要计算的属性,您可以在 #[pymethods] 代码块中定义 #[getter] 和 #[setter] 函数。
    1. <font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">#[getter]</font><font style="color:rgb(48, 25, 0);background-color:rgb(246, 247, 246);">#[setter]</font> 属性都接受一个参数。若指定该参数,则将其用作属性名。

#[classmethod]

类似于python的@classmethod

#[staticmethod]

类似于python的@staticmathod

注册自定义类

1
2
3
4
5
#[pymodule]
fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<Number>()?;
Ok(())
}