pyo3:https://github.com/PyO3/pyo3

maturin:https://github.com/PyO3/maturin

两种方式:

  • 在python代码中调用rust编写的模块
  • 在rust代码中运行python

一、在python代码中调用rust编写的模块

PyO3 可用于生成本机 Python 模块。最简单的方法是使用 maturinmaturin 是一个用很少的配置可以构建和发布基于 Rust 的 Python 包的工具。通过以下步骤安装 maturin ,使用它生成并构建新的 Python 包,然后可以运行 Python 导入并执行包中的函数。

1 一个简单的示例

1.1 虚拟环境创建和maturin安装

首先,按照以下命令创建一个包含新 Python virtualenv 的新目录,并使用 Python 的包管理器 pipmaturin 安装到 virtualenv 中:

1
2
3
4
5
mkdir string_sum
cd string_sum
python -m venv .env
source .env/bin/activate # 如果是windows,这里需要改成 .env/Scripts/activate
pip install maturin

在这个 string_sum 目录中,现在运行 maturin init 。这将生成新的包源。当命令行提示选择要使用的绑定时,选择 pyo3 绑定:

1
2
3
maturin init
✔ 🤷 What kind of bindings to use? · pyo3
✨ Done! New project created string_sum

此命令生成的最重要的文件是 Cargo.tomllib.rs ,它们大致如下所示:

Cargo.toml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[package]
name = "string_sum"
version = "0.1.0"
edition = "2021"

[lib]
# The name of the native library. This is the name which will be used in Python to import the
# library (i.e. `import string_sum`). If you change this, you must also change the name of the
# `#[pymodule]` in `src/lib.rs`.
name = "string_sum"
# "cdylib" is necessary to produce a shared library for Python to import from.
#
# Downstream Rust code (including code in `bin/`, `examples/`, and `tests/`) will not be able
# to `use string_sum;` unless the "rlib" or "lib" crate type is also included, e.g.:
# crate-type = ["cdylib", "rlib"]
crate-type = ["cdylib"]

[dependencies]
pyo3 = { version = "0.20.0", features = ["extension-module"] }

src/lib.rs

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

/// Formats the sum of two numbers as string.
#[pyfunction]
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
Ok((a + b).to_string())
}

/// A Python module implemented in Rust. The name of this function must match
/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to
/// import the module.
#[pymodule]
fn string_sum(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
Ok(())
}

1.2 编译并在python中运行

最后,运行 maturin develop 。这将构建包并将其安装到之前创建和激活的 Python virtualenv 中。然后该包就可以从 python 中使用了:

1
2
3
4
5
6
maturin develop
# 这会编译rust代码并构建python模块
python
>>> import string_sum
>>> string_sum.sum_as_string(5, 20)
'25'

要对包进行更改,只需编辑 Rust 源代码,然后重新运行 maturin develop 进行重新编译。注意,只要rust代码被更改,就要重新编译。

2 python模块创建

现在,让我们看看刚才自动生成的代码,并逐渐介绍其用法。

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

/// Formats the sum of two numbers as string.
#[pyfunction]
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
Ok((a + b).to_string())
}

/// A Python module implemented in Rust. The name of this function must match
/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to
/// import the module.
#[pymodule]
fn string_sum(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
Ok(())
}

在这里,我们通过 #[pymodule] 过程宏创建模块,它负责将模块的初始化函数导出到 Python。模块的名称默认这里 Rust 函数的名称。你可以使用 #[pyo3(name = "custom_name")] 覆盖模块名称:

1
2
3
4
5
6
#[pymodule]
#[pyo3(name = "custom_name")]
fn my_extension(py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(double, m)?)?;
Ok(())
}

注意,如果修改了模块名称,也要同时修改Cargo.tomllib.name,使它们相匹配,否则python无法导入该模块。

1
2
3
[lib]
name = "custom_name" # 修改这里
crate-type = ["cdylib"]

2.1 子模块创建

可以使用 PyModule.add_submodule() 在单个扩展模块中创建模块层次结构。例如,可以定义模块 parent_moduleparent_module.child_module

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

#[pymodule]
fn parent_module(py: Python<'_>, m: &PyModule) -> PyResult<()> {
register_child_module(py, m)?;
Ok(())
}

fn register_child_module(py: Python<'_>, parent_module: &PyModule) -> PyResult<()> {
let child_module = PyModule::new(py, "child_module")?;
child_module.add_function(wrap_pyfunction!(func, child_module)?)?;
parent_module.add_submodule(child_module)?;
Ok(())
}

#[pyfunction]
fn func() -> String {
"func".to_string()
}

这里注意,此处并没有定义python package,因此python代码不允许使用 from parent_module import child_module 直接导入子模块。有关更多信息,请参阅 #759#1517.

嵌套模块上不需要添加 #[pymodule] ,仅在顶层模块上需要添加 #[pymodule]

3 python函数创建

#[pyfunction] 属性用于从 Rust 函数定义 Python 函数。定义后,需要使用 wrap_pyfunction! 宏将该函数添加到模块中。比如之前默认生成的代码:

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

#[pyfunction]
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
Ok((a + b).to_string())
}

#[pymodule]
fn string_sum(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
Ok(())
}

这段代码在名为 string_sum 的 Python 模块中定义了名为 sum_as_string 的函数。

3.1 功能选项

#[pyo3] 属性可用于修改生成的 Python 函数的属性。它可以采用以下选项的任意组合:

  • #[pyo3(name = "...")],覆盖默认的python函数名称(默认是对应的rust函数名)。

  • #[pyo3(signature = (...))],定义 Python 中的函数签名。请参阅函数签名

  • #[pyo3(text_signature = "...")],覆盖 Python 工具中可见的 PyO3 生成的函数签名(例如通过 inspect.signature )。请参阅使函数签名可供 Python 使用

  • #[pyo3(pass_module)],设置此选项以使 PyO3 将包含模块作为函数的第一个参数传递。然后就可以在函数体中使用该模块了。第一个参数必须是 &PyModule 类型。

    以下示例创建一个函数 pyfunction_with_module ,它返回包含模块的名称(即 module_with_fn ):

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

    #[pyfunction]
    #[pyo3(pass_module)]
    fn pyfunction_with_module(module: &PyModule) -> PyResult<&str> {
    module.name()
    }

    #[pymodule]
    fn module_with_fn(py: Python<'_>, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?)
    }
  • #[pyo3(from_py_with = "...")],在选项上设置此选项以指定自定义函数,以将函数参数从 Python 转换为所需的 Rust 类型,而不是使用默认的 FromPyObject 提取。#[pyo3] 属性可用于各个参数,以修改生成函数中它们的属性。函数签名必须是 fn(&PyAny) -> PyResult<T> ,其中 T 是参数的 Rust 类型。以下示例使用 from_py_with 将输入 Python 对象转换为其长度:

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

    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
    }

3.2 #[pyfn] 简写

#[pyfunction]wrap_pymodule! 有一个简写:函数可以放置在模块定义中并用 #[pyfn] 注释。为了简化 PyO3,预计 #[pyfn] 可能会在未来版本中删除(参见#694)。

#[pyfn] 的示例如下:

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

#[pymodule]
fn my_extension(py: Python<'_>, m: &PyModule) -> PyResult<()> {
#[pyfn(m)]
fn double(x: usize) -> usize {
x * 2
}

Ok(())
}

#[pyfn(m)] 只是 #[pyfunction] 的语法糖,上面的代码扩展为以下内容:

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

#[pymodule]
fn my_extension(py: Python<'_>, m: &PyModule) -> PyResult<()> {
#[pyfunction]
fn double(x: usize) -> usize {
x * 2
}

m.add_function(wrap_pyfunction!(double, m)?)?;
Ok(())
}

3.3 函数签名

#[pyfunction] 属性还接受参数来控制生成的 Python 函数如何接受参数。就像在 Python 中一样,参数可以是仅限位置的、仅限关键字的,或者接受其中任何一个。 *args 列表和 **kwargs 字典也可以被接受。这些参数也适用于 #[pymethods] ,它将在后面进行介绍。

与 Python 一样,默认情况下 PyO3 接受所有参数作为位置参数或关键字参数。默认情况下,大多数参数都是必需的,但尾随 Option<_> 参数除外,这些参数隐式指定了默认值 None 。有两种方法可以修改此行为:

有两种方法可以修改此行为:

  • #[pyo3(signature = (...))] 选项允许使用 Python 语法编写签名。
  • 额外的参数直接传递给 #[pyfunction] 。(已弃用)

使用 #[pyo3(signature = (…))]

例如,下面是一个接受任意关键字参数(Python 语法中的 **kwargs )并返回传递的数字的函数:

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

#[pyfunction]
#[pyo3(signature = (**kwds))]
fn num_kwds(kwds: Option<&PyDict>) -> usize {
kwds.map_or(0, |dict| dict.len())
}

#[pymodule]
fn module_with_functions(py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(num_kwds, m)?).unwrap();
Ok(())
}

就像在 Python 中一样,以下构造可以是签名的一部分:

  • / :仅位置参数分隔符, / 之前定义的每个参数都是仅位置参数。
  • * :var 参数分隔符, * 之后定义的每个参数都是仅关键字参数。
  • *args :“args”是 var args。 args 参数的类型必须是 &PyTuple
  • **kwargs :“kwargs”接收关键字参数。 kwargs 参数的类型必须是 Option<&PyDict>
  • arg=Value :具有默认值的参数。如果 arg 参数在 var 参数之后定义,则它被视为仅关键字参数。请注意, 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
26
27
28
29
30
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: &PyTuple,
name: &str,
py_kwargs: Option<&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,
)
}

fn make_change(&mut self, num: i32) -> PyResult<String> {
self.num = num;
Ok(format!("num={}", self.num))
}
}

Python 类型的参数不能作为签名的一部分:

1
2
3
4
5
#[pyfunction]
#[pyo3(signature = (lambda))]
pub fn simple_python_bound_function(py: Python<'_>, lambda: PyObject) -> PyResult<()> {
Ok(())
}

注意: /* 参数(如果包含)的位置控制处理位置和关键字参数的系统。在Python中:

1
2
3
4
5
6
import mymodule

mc = mymodule.MyClass()
print(mc.method(44, False, "World", 666, x=44, y=55))
print(mc.method(num=-1, name="World"))
print(mc.make_change(44, False))

输出:

1
2
3
4
py_args=('World', 666), py_kwargs=Some({'x': 44, 'y': 55}), name=Hello, num=44
py_args=(), py_kwargs=None, name=World, num=-1
num=44
num=-1

要使用像 struct 这样的rust关键字作为函数参数,请在签名和函数定义中使用“原始标识符”语法 r#struct

1
2
3
4
#[pyfunction(signature = (r#struct = "foo"))]
fn function_with_keyword(r#struct: &str) {
/* ... */
}

尾随可选参数

为了方便起见,没有 #[pyo3(signature = (...))] 选项的函数会将尾随 Option<T> 参数视为具有默认值 None 。在下面的示例中,PyO3 将创建带有 increment(x, amount=None) 签名的 increment

1
2
3
4
5
6
7
8
9
use pyo3::prelude::*;

/// Returns a copy of `x` increased by `amount`.
///
/// If `amount` is unspecified or `None`, equivalent to `x + 1`.
#[pyfunction]
fn increment(x: u64, amount: Option<u64>) -> u64 {
x + amount.unwrap_or(1)
}

要使尾随 Option<T> 参数成为必需,但仍接受 None ,需要添加 #[pyo3(signature = (...))] 注释。对于上面的示例,这将是 #[pyo3(signature = (x, amount))]

1
2
3
4
5
#[pyfunction]
#[pyo3(signature = (x, amount))]
fn increment(x: u64, amount: Option<u64>) -> u64 {
x + amount.unwrap_or(1)
}

为了避免混淆,当 Option<T> 参数被不是 Option<T> 的参数包围时,PyO3 需要 #[pyo3(signature = (...))]

使函数签名可供 Python 使用

函数签名通过 __text_signature__ 属性向 Python 公开。 PyO3 直接从 Rust 函数自动为每个 #[pyfunction] 和所有 #[pymethods] 生成此值,同时考虑使用 #[pyo3(signature = (...))] 选项完成的任何覆盖。

此自动生成只能显示字符串、整数、布尔类型和 None 的默认参数值。任何其他默认参数将显示为 ... 。 ( .pyi 类型存根文件通常也以相同的方式使用 ... 作为默认参数。)

如果需要调整自动生成的签名,可以使用 #[pyo3(text_signature)] 选项覆盖它。)

下面的示例创建一个函数 add ,它接受两个仅位置参数 ab ,其中 b 的默认值为零。

1
2
3
4
5
6
7
8
use pyo3::prelude::*;

/// This function adds two unsigned 64-bit integers.
#[pyfunction]
#[pyo3(signature = (a, b=0, /))]
fn add(a: u64, b: u64) -> u64 {
a + b
}

以下从 IPython 输出演示了如何从 Python 工具中看到生成的签名:

1
2
3
4
5
6
>>> pyo3_test.add.__text_signature__
'(a, b=..., /)'
>>> pyo3_test.add?
Signature: pyo3_test.add(a, b=0, /)
Docstring: This function adds two unsigned 64-bit integers.
Type: builtin_function_or_method

覆盖生成的签名

使用#[pyo3(text_signature = "(<some signature>)")] 属性可用于覆盖默认生成的签名。

在下面的代码片段中,文本签名属性用于包含参数 b 的默认值 0 ,而不是自动生成的默认值 ...

1
2
3
4
5
6
7
8
use pyo3::prelude::*;

/// This function adds two unsigned 64-bit integers.
#[pyfunction]
#[pyo3(signature = (a, b=0, /), text_signature = "(a, b=0, /)")]
fn add(a: u64, b: u64) -> u64 {
a + b
}

PyO3 将包含未修改的注释内容作为 __text_signature 。下面显示了 IPython 现在将如何呈现此内容(b 的默认值 0):

1
2
3
4
5
6
>>> pyo3_test.add.__text_signature__
'(a, b=0, /)'
>>> pyo3_test.add?
Signature: pyo3_test.add(a, b=0, /)
Docstring: This function adds two unsigned 64-bit integers.
Type: builtin_function_or_method

如果根本不需要签名, #[pyo3(text_signature = None)] 将禁用内置签名。下面的代码片段演示了它的用法:

1
2
3
4
5
6
7
8
use pyo3::prelude::*;

/// This function adds two unsigned 64-bit integers.
#[pyfunction]
#[pyo3(signature = (a, b=0, /), text_signature = None)]
fn add(a: u64, b: u64) -> u64 {
a + b
}

现在函数的 __text_signature__ 将被设置为 None ,并且 IPython 将不会在帮助中显示任何签名:

1
2
3
4
5
>>> pyo3_test.add.__text_signature__ == None
True
>>> pyo3_test.add?
Docstring: This function adds two unsigned 64-bit integers.
Type: builtin_function_or_method

3.4 错误处理

下面会回顾 Rust 错误处理的一些背景知识以及 PyO3 如何将其与 Python 异常集成。这里先做个简单的介绍,我们会在后面部分继续讨论关于 Python 异常的内容,其中更详细地介绍了异常类型。

代表python异常

Rust 代码使用通用 Result<T, E> 枚举来传播错误。错误类型 E 由代码作者选择来描述可能发生的错误。

PyO3 具有代表 Python 异常的 PyErr 类型。如果 PyO3 API 可能导致引发 Python 异常,则 API 的返回类型将为 PyResult<T> ,这是类型 Result<T, PyErr> 的别名。

总结:

  • 当 PyO3 引发并捕获 Python 异常时,异常将存储在 PyResultErr 中。
  • 然后,通过 Rust 代码传递 Python 异常会使用所有“正常”技术,例如 ? 运算符,并以 PyErr 作为错误类型。
  • 最后,当 PyResult 通过 PyO3 从 Rust 跨回到 Python 时,如果结果是 Err ,则将引发包含的异常。

至于Rust部分的异常处理(比如?)等相关内容,我们假设你已经了解。

从函数中引发异常

如上所述,当包含 ErrPyResult 从 Rust 跨越到 Python 时,PyO3 将引发其中包含的异常。

因此,要从 #[pyfunction] 引发异常,需要将返回类型 T 更改为 PyResult<T> 。当函数返回 Err 时,它将引发 Python 异常。 (只要错误 E 具有 PyErrFrom 转换,就可以使用其他 Result<T, E> 类型 )

这也适用于 #[pymethods] 中的函数。

例如,当输入为负数时,以下 check_positive 函数会引发 ValueError

1
2
3
4
5
6
7
8
9
10
11
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(())
}
}

所有内置的Python异常类型都在 pyo3::exceptions 模块中定义。他们有一个 new_err 构造函数来直接构建 PyErr ,如上面的示例所示。

自定义 Rust 错误类型

只要实现了 std::from::From<E> for PyErr ,PyO3 就会自动将 #[pyfunction] 返回的 Result<T, E> 转换为 PyResult<T> 。 Rust 标准库中的许多错误类型都有以这种方式定义的 From 转换。

如果你正在处理的类型 E 是在第三方 crate 中定义的,请参阅下一节,了解处理此错误的方法。

以下示例利用 From<ParseIntError> for PyErr 的实现来引发将字符串解析为整数时遇到的异常:

1
2
3
4
5
6
use std::num::ParseIntError;

#[pyfunction]
fn parse_int(x: &str) -> Result<usize, ParseIntError> {
x.parse()
}

当传递一个不包含浮点数的字符串时,引发的异常将如下所示:

1
2
3
4
>>> parse_int("bar")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid digit found in string

作为更完整的示例,以下代码片段定义了一个名为 CustomIOError 的 Rust 错误。然后它定义一个 From<CustomIOError> for PyErr ,它返回一个代表 Python 的 OSErrorPyErr 。因此,它可以直接在 #[pyfunction] 的结果中使用此错误,如果必须将其传播到 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
37
38
39
40
41
42
43
44
45
use pyo3::exceptions::PyOSError;
use pyo3::prelude::*;
use std::fmt;

#[derive(Debug)]
struct CustomIOError;

impl std::error::Error for CustomIOError {}

impl fmt::Display for CustomIOError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Oh no!")
}
}

impl std::convert::From<CustomIOError> for PyErr {
fn from(err: CustomIOError) -> PyErr {
PyOSError::new_err(err.to_string())
}
}

pub struct Connection {/* ... */}

fn bind(addr: String) -> Result<Connection, CustomIOError> {
if &addr == "0.0.0.0" {
Err(CustomIOError)
} else {
Ok(Connection{ /* ... */})
}
}

#[pyfunction]
fn connect(s: String) -> Result<(), CustomIOError> {
bind(s)?;
// etc.
Ok(())
}

fn main() {
Python::with_gil(|py| {
let fun = pyo3::wrap_pyfunction!(connect, py).unwrap();
let err = fun.call1(("0.0.0.0",)).unwrap_err();
assert!(err.is_instance_of::<PyOSError>(py));
});
}

如果需要延迟构造 Python 异常实例,则可以实现 PyErrArguments 特征而不是 From 。在这种情况下,实际的异常参数创建会被延迟,直到需要 PyErr 为止。 最后要注意的是,任何具有 From 转换的错误 E 都可以与 ? (“try”)运算符一起使用。上述 parse_int 的替代实现如下,它返回 PyResult

1
2
3
4
5
6
use pyo3::prelude::*;

fn parse_int(s: String) -> PyResult<usize> {
let x = s.parse()?;
Ok(x)
}

第三方 Rust 错误类型

Rust 编译器不允许在定义类型的包之外实现类型的特征,这被称为“孤儿规则”。

给定在第三方代码中定义的类型 OtherError ,有两种主要策略可将其与 PyO3 集成:

  • 创建一个新类型包装器,例如 MyOtherError 。然后实现 From<MyOtherError> for PyErr (或 PyErrArguments ),以及 MyOtherErrorFrom<OtherError>
  • 使用 Rust 的结果组合符(例如 map_err )自由编写代码,将 OtherError 转换为所需的任何内容。这在每次使用时都需要样板,但提供了无限的灵活性。

为了进一步详细说明新类型策略,关键技巧是从 #[pyfunction] 返回 Result<T, MyOtherError> 。这意味着 PyO3 将利用 From<MyOtherError> for PyErr 创建 Python 异常,而 #[pyfunction] 实现可以使用 ?OtherError 自动转换为 MyOtherError

以下示例针对一些虚构的第三方包 some_crate 演示了这一点,其中函数 get_x 返回 Result<i32, OtherError>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use pyo3::prelude::*;
use pyo3::exceptions::PyValueError;
use some_crate::{OtherError, get_x};

struct MyOtherError(OtherError);

impl From<MyOtherError> for PyErr {
fn from(error: MyOtherError) -> Self {
PyValueError::new_err(error.0.message())
}
}

impl From<OtherError> for MyOtherError {
fn from(other: OtherError) -> Self {
Self(other)
}
}

#[pyfunction]
fn wrapped_get_x() -> Result<i32, MyOtherError> {
// get_x is a function returning Result<i32, OtherError>
let x: i32 = get_x()?;
Ok(x)
}

4 python类

PyO3 公开了一组由 Rust 的 proc 宏系统支持的属性,用于将 Python 类定义为 Rust 结构。

主要属性是 #[pyclass] ,它被放置在 Rust structenum (类似 C 的枚举类型)上以为其生成 Python 类型。它们通常还会有一个带 #[pymethods] 注释的 impl 结构块,用于为生成的 Python 类型定义 Python 方法和常量。 (如果启用 multiple-pymethods features,则允许每个 #[pyclass] 具有多个 #[pymethods] 块。) #[pymethods] 也可能具有 Python 实现魔法方法,例如 __str__

4.1 定义一个新类

要定义自定义 Python 类,将 #[pyclass] 属性添加到 Rust 结构体或枚举。

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::prelude::*;

#[pyclass]
struct Integer {
inner: i32,
}

// A "tuple" struct
#[pyclass]
struct Number(i32);

// PyO3 supports custom discriminants in enums
#[pyclass]
enum HttpResponse {
Ok = 200,
NotFound = 404,
Teapot = 418,
// ...
}

#[pyclass]
enum MyEnum {
Variant,
OtherVariant = 30, // PyO3 supports custom discriminants.
}

上面的示例为 MyClassMyEnum 生成 PyTypeInfoPyClass 的实现。

一些限制

为了将 Rust 类型与 Python 集成,PyO3 需要对可以使用 #[pyclass] 的类型进行一些限制。特别是,它们必须没有生命周期参数,没有通用参数,并且必须实现 Send 。下面解释每一个的原因。

没有生命周期参数

Rust 编译器使用 Rust 生命周期来推断程序的内存安全性。它们只是编译时的概念;无法从像 Python 这样的动态语言在运行时访问 Rust 生命周期。

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

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

无泛型参数

带有泛型参数 T 的 Rust struct Foo<T> 每次与 T 的不同具体类型一起使用时都会生成新的编译实现。这些新的实现由编译器在每个使用点生成。这与在 Python 中包装 Foo 的方式不兼容,后者需要与 Python 解释器集成的 Foo 的单个编译实现。

必须实现Send

由于 Python 对象由 Python 解释器在线程之间自由共享,因此无法保证哪个线程最终会删除该对象。因此,所有用 #[pyclass] 注释的类型都必须实现 Send (除非用 #[pyclass(unsendable)] 注释)。

4.2 构造函数

默认情况下,无法从 Python 代码创建自定义类的实例。要声明构造函数,你需要定义一个方法并使用 #[new] 属性对其进行注释。只能指定Python的 __new__ 方法, __init__ 不可用。

1
2
3
4
5
6
7
#[pymethods]
impl Number {
#[new]
fn new(value: i32) -> Self {
Number(value)
}
}

如果你的 new 方法可能失败,可以返回 PyResult<Self>

1
2
3
4
5
6
7
8
9
10
11
#[pymethods]
impl Nonzero {
#[new]
fn py_new(value: i32) -> PyResult<Self> {
if value == 0 {
Err(PyValueError::new_err("cannot be zero"))
} else {
Ok(Nonzero(value))
}
}
}

如果你想返回现有对象(例如,因为你的 new 方法缓存了它返回的值), new 可以返回 pyo3::Py<Self>

在此处,Rust 方法名称并不重要,因此你仍然可以使用 new() 作为 Rust 级别的构造函数。

如果没有声明带有 #[new] 标记的方法,则只能从 Rust 创建对象实例,而不能从 Python 创建对象实例。

4.3 将类添加到模块中

下面就是创建模块并向其中添加我们的类:

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

4.4 自定义类

#[pyclass] 可以与以下参数一起使用:

参数描述
crate = "some::path"导入 pyo3 crate的路径(如果无法在 ::pyo3 处访问)。
dict为此类的实例提供一个空的 __dict__ 来存储自定义属性。
extends = BaseType使用自定义基类。默认为PyAny
freelist = N实现大小为 N 的空闲列表。这可以提高经常快速连续创建和删除的类型的性能。你可以分析你的代码以查看 freelist 是否适合你。
frozen声明你的 pyclass 是不可变的。它消除了检索 Rust 结构的共享引用时的借用检查器开销,但禁用了获取可变引用的能力。
get_all为 pyclass 的所有字段生成 getter
mapping通知 PyO3 该类是 Mapping ,因此将其序列 C-API 槽的实现留空。
module = "module_name"Python 代码将看到该模块中定义的类。默认为 builtins
name = "python_name"设置 Python 看到的此类的名称。默认为 Rust 中的名称。
rename_all = "renaming_rule"将重命名规则应用于结构体的每个 getter 和 setter 或枚举的每个变体。可能的值为:“camelCase”、“kebab-case”、“lowercase”、“PascalCase”、“SCREAMING-KEBAB-CASE”、“SCREAMING_SNAKE_CASE”、“snake_case”、“UPPERCASE”。
sequence通知 PyO3 该类是 Sequence ,因此将其 C-API 映射长度槽保留为空。
set_all为 pyclass 的所有字段生成 setter
subclass允许其他Python类和 #[pyclass] 继承自该类。枚举不能被子类化。
text_signature = "(arg1, arg2, ...)"设置 Python 类的 __new__ 方法的文本签名。
unsendable如果你的结构体不是 Send ,则为必需。不要使用 unsendable ,而是考虑以线程安全的方式实现你的结构体,例如将 Rc 替换为 Arc 。通过使用 unsendable ,你的类在被另一个线程访问时将会出现panic。
weakref允许此类被弱引用

所有这些参数都可以直接在 #[pyclass(...)] 注释上传递,也可以作为一个或多个附带的 #[pyo3(...)] 注释传递,例如:

1
2
3
4
5
6
7
8
// Argument supplied directly to the `#[pyclass]` annotation.
#[pyclass(name = "SomeName", subclass)]
struct MyClass {}

// Argument supplied as a separate annotation.
#[pyclass]
#[pyo3(name = "SomeName", subclass)]
struct MyClass {}

4.5 类的继承和更高级用法

有关更多类的继承和高级用法,请参考官方文档

5 类型转换

当编写可从 Python 调用的函数(例如 #[pyfunction] 或 #[pymethods] 块中)时,函数参数需要实现 FromPyObject 特征,返回值需要实现IntoPy<PyObject>特征。

PyO3默认已经为部分rust类型实现了这些特征,它们对应python的类型。我们将在下文中的表格里展示。

5.1 接受函数参数

当接受函数参数时,可以使用 Rust 库类型或 PyO3 提供的 Python 原生类型。

下表包含 Python 类型以及接受它们的相应函数参数类型:

PythonRustRust (Python 原生类型)
object-&PyAny
strString, Cow<str>, &str, OsString, PathBuf, Path&PyString, &PyUnicode
bytesVec<u8>, &[u8], Cow<[u8]>&PyBytes
boolbool&PyBool
inti8, u8, i16, u16, i32, u32, i64, u64, i128, u128, isize, usize, num_bigint::BigInt1, num_bigint::BigUint1&PyLong
floatf32, f64&PyFloat
complexnum_complex::Complex2&PyComplex
list[T]Vec<T>&PyList
dict[K, V]HashMap<K, V>, BTreeMap<K, V>, hashbrown::HashMap<K, V>3, indexmap::IndexMap<K, V>4&PyDict
tuple[T, U](T, U), Vec<T>&PyTuple
set[T]HashSet<T>, BTreeSet<T>, hashbrown::HashSet<T>3&PySet
frozenset[T]HashSet<T>, BTreeSet<T>, hashbrown::HashSet<T>3&PyFrozenSet
bytearrayVec<u8>, Cow<[u8]>&PyByteArray
slice-&PySlice
type-&PyType
module-&PyModule
collections.abc.Buffer-PyBuffer<T>
datetime.datetime-&PyDateTime
datetime.date-&PyDate
datetime.time-&PyTime
datetime.tzinfo-&PyTzInfo
datetime.timedelta-&PyDelta
decimal.Decimalrust_decimal::Decimal5-
ipaddress.IPv4Addressstd::net::IpAddr, std::net::IpV4Addr-
ipaddress.IPv6Addressstd::net::IpAddr, std::net::IpV6Addr-
os.PathLikePathBuf, Path&PyString, &PyUnicode
pathlib.PathPathBuf, Path&PyString, &PyUnicode
typing.Optional[T]Option<T>-
typing.Sequence[T]Vec<T>&PySequence
typing.Mapping[K, V]HashMap<K, V>, BTreeMap<K, V>, hashbrown::HashMap<K, V>3, indexmap::IndexMap<K, V>4&PyMapping
typing.Iterator[Any]-&PyIterator
typing.Union[...]#[derive(FromPyObject)]-

还有一些与 GIL 和 Rust 定义的 #[pyclass] 相关的特殊类型:

类型描述
PythonGIL 令牌,用于传递给 PyO3 构造函数以证明 GIL 的所有权
Py<T>与 GIL 生命周期隔离的 Python 对象。可以发送到其他线程。
PyObjectPy<PyAny> 的别名
&PyCell<T>Python 拥有的 #[pyclass] 值。
PyRef<T>#[pyclass] 不可变引用。
PyRefMut<T>#[pyclass] 可变引用。

有关接受 #[pyclass] 值作为函数参数的更多详细信息,请参阅有关 Python 类的部分

使用 Rust 库类型还是 Python 原生类型?

与使用 Python 原生类型相比,使用 Rust 库类型作为函数参数会产生转换成本。使用 Python 原生类型几乎是零成本(它们只需要类似于 Python 内置函数 isinstance() 的类型检查)。

然而,一旦支付了转换成本,Rust 标准库类型就会提供许多优点:

  • 可以用接近原生 Rust 速度的代码编写功能(没有 Python 的运行时成本)。
  • 可以与 Rust 生态系统的其他部分获得更好的互操作性。
  • 可以使用 Python::allow_threads 释放 Python GIL,并让其他 Python 线程在执行 Rust 代码时继续执行。
  • 还可以从更严格的类型检查中受益。例如,你可以指定 Vec<i32> ,它只接受包含整数的 Python list 。 Python 原生的等价物 &PyList 则会接受包含任何类型的 Python 对象的 Python list

对于大多数 PyO3 使用来说,为了获得这些优点,转换成本是值得的。与往常一样,如果你不确定它在你的代码中使用是否值得,请对其进行基准测试!

5.2 将 Rust 值返回给 Python

当从可从 Python 调用的函数返回值时,可以零成本使用 Python 原生类型( &PyAny&PyDict 等)。

由于这些类型是引用,因此在某些情况下 Rust 编译器可能会要求生命周期注释。如果是这种情况,你应该使用 Py<PyAny>Py<PyDict> 等代替,这也是零成本的。对于所有这些 Python 原生类型 T ,可以通过 .into() 转换从 T 创建 Py<T>

如果你的函数容易出错,它应该返回 PyResult<T>Result<T, E> ,其中 E 需要实现 From<E> for PyErr 。如果返回 Err 变体,这将引发 Python 异常。

最后,以下 Rust 类型也可以转换为 Python 作为返回值:

Rust 类型生成的 Python 类型
Stringstr
&strstr
boolbool
任何数字类型 (i32, u32, usize, 等)int
f32, f64float
Option<T>Optional[T]
(T, U)Tuple[T, U]
Vec<T>List[T]
Cow<[u8]>bytes
HashMap<K, V>Dict[K, V]
BTreeMap<K, V>Dict[K, V]
HashSet<T>Set[T]
BTreeSet<T>Set[T]
&PyCell<T: PyClass>T
PyRef<T: PyClass>T
PyRefMut<T: PyClass>T

5.3 与类型转换相关的特征

PyO3 提供了一些方便的特征来在 Python 类型和 Rust 类型之间进行转换。

.extract() 和 FromPyObject 特征

将 Python 对象转换为 Rust 值的最简单方法是使用 .extract() 。如果转换失败,它会返回一个带有类型错误的 PyResult ,所以通常你会使用类似如下的方式:

1
let v: Vec<i32> = list.extract()?;

此方法适用于许多 Python 对象类型,并且可以生成多种 Rust 类型,你可以在 FromPyObject 的实现者列表中查看这些类型。

FromPyObject 特征也可以为包装为 Python 对象的自定义的 Rust 类型实现(请参阅有关类的章节)。为了能够对可变引用进行操作并满足 Rust 的非别名可变引用规则,你必须提取 PyO3 引用包装器 PyRefPyRefMut 。它们的工作方式类似于 std::cell::RefCell 的引用包装器,并确保(在运行时)允许 Rust 借用。

派生 FromPyObject

如果成员类型本身实现 FromPyObject ,则可以自动派生多种结构体和枚举的 FromPyObject 。这甚至包括具有泛型类型 T: FromPyObject 的成员。但注意,不支持空的枚举、枚举变体和结构体的派生。

为结构体派生 FromPyObject

派生生成的代码将尝试访问 Python 对象上的属性 my_string (即 obj.getattr("my_string") ),并调用该属性上的 extract()

1
2
3
4
5
6
use pyo3::prelude::*;

#[derive(FromPyObject)]
struct RustyStruct {
my_string: String,
}

通过在字段上设置 #[pyo3(item)] 属性,PyO3 将尝试通过调用 Python 对象上的 get_item 方法来提取值。

1
2
3
4
5
6
7
use pyo3::prelude::*;

#[derive(FromPyObject)]
struct RustyStruct {
#[pyo3(item)]
my_string: String,
}

传递给 getattrget_item 的参数也可以配置:

1
2
3
4
5
6
7
8
9
use pyo3::prelude::*;

#[derive(FromPyObject)]
struct RustyStruct {
#[pyo3(item("key"))]
string_in_mapping: String,
#[pyo3(attribute("name"))]
string_attr: String,
}

这尝试从属性 name 中提取 string_attr ,并从具有键 "key" 的映射中提取 string_in_mappingattribute 的参数仅限于非空字符串文字,而 item 可以采用任何实现 ToBorrowedObject 的有效文字。

可以在结构上使用 #[pyo3(from_item_all)] 来使用 get_item 方法提取每个字段。在这种情况下,你不能在任何字段上使用 #[pyo3(attribute)] 或几乎不使用 #[pyo3(item)] 。但是,仍然允许使用 #[pyo3(item("key"))] 指定字段的键。

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

#[derive(FromPyObject)]
#[pyo3(from_item_all)]
struct RustyStruct {
foo: String,
bar: String,
#[pyo3(item("foobar"))]
baz: String,
}

更多的派生方法

更多的派生方法,请参阅这里

IntoPy<T>

此特征定义了 Rust 类型到 python 的转换。它通常实现为 IntoPy<PyObject> ,这是从 #[pyfunction]#[pymethods] 返回值所需的特征。

PyO3 中的所有类型都实现此特征,就像不使用 extends#[pyclass] 一样。

有时,你可能会选择为映射到 Python 类型但没有唯一 Python 类型的自定义类型实现此功能。

1
2
3
4
5
6
7
8
9
use pyo3::prelude::*;

struct MyPyObjectWrapper(PyObject);

impl IntoPy<PyObject> for MyPyObjectWrapper {
fn into_py(self, py: Python<'_>) -> PyObject {
self.0
}
}

ToPyObject特征

ToPyObject 是一个转换特征,允许将各种对象转换为 PyObjectIntoPy<PyObject> 具有相同的用途,只不过它消耗 self

6 GIL 生命周期、可变性和 Python 对象类型

乍一看,PyO3 提供了大量不同的类型,可用于包装或引用 Python 对象。下面我们深入研究细节并概述其预期含义,并举例说明每种类型的最佳使用方式。

6.1 Python GIL、可变性和 Rust 类型

由于 Python 没有所有权的概念,并且一切皆对象,因此任何 Python 对象都可以被引用任意次数,并且允许从任何引用进行转换。因此,Python 解释器不是线程安全的。为了在多线程场景下保护Python解释器,需要有一个全局锁,必须持有全局解释器锁(以下简称GIL)才能安全地与Python对象交互,它确保只有一个线程可以同时使用Python解释器及其API,而非Python操作(系统调用和扩展代码)可以解锁GIL 。

在 PyO3 中,获取 GIL 是通过获取 Python<'py> 类型来实现的,当你获取 GIL 时,你会获得一个 Python 标记令牌,该标记令牌携带持有 GIL 的生命周期,并且所有借用的对 Python 对象的引用也携带此生命周期。这将静态地确保你在释放锁后永远不能使用 Python 对象,它将在编译时被捕获,并且你的程序将无法通过编译。

具体来说,持有GIL有三个目的:

  • 它为Python解释器提供了一些全局API,例如 eval
  • 它可以传递给需要持有 GIL 证明的函数,例如 Py::clone_ref
  • 它的生命周期可用于创建隐式保证持有 GIL 的 Rust 引用,例如 &'py PyAny

后两点是 PyO3 中的某些 API 需要 py: Python 参数而其他 API 不需要的原因。

用于 Python 对象的 PyO3 API 的编写方式不需要可变 Rust 引用来进行诸如 PyList::append 之类的可变操作,而是共享引用(反过来,只能通过 Python<'_> 具有 GIL 生命周期)就足够了。

然而,包装为 Python 对象(称为 pyclass 类型)的 Rust 结构体通常需要 &mut 访问。由于 GIL,PyO3 可以保证对它们的线程安全访问,但一旦对象的所有权传递给 Python 解释器,它就不能静态保证 &mut 引用的唯一性,确保在运行时使用 &mut 引用完成引用。 b3> ,这是与 std::cell::RefCell 非常相似的解决方式。

获取Python令牌

以下是获取 Python 令牌的推荐方法(按优先顺序排列):

  • 在用 #[pyfunction]#[pymethods] 注解的函数或方法中,你可以将其声明为参数,当 Python 代码调用它时,PyO3 将传入令牌。
  • 如果你已经有一些生命周期绑定到 GIL 的东西,例如 &PyAny ,可以使用其 .py() 方法来获取令牌。
  • 当你需要自己获取 GIL 时,例如从 Rust 调用 Python 代码时,你应该调用 Python::with_gil 来执行此操作,并将代码作为闭包传递给它。

避免死锁

Python 解释器可以在函数调用期间(例如导入模块)临时释放 GIL。一般来说,你不需要关注这个,因为在返回 Rust 代码之前会重新获取 GIL:

1
2
3
`Python` exists   |=====================================|
GIL actually held |==========| |================|
Rust code running |=======| |==| |======|

当尝试在持有 GIL 的同时锁定 rust 的 Mutex互斥锁时,此行为可能会导致死锁,假设我们有两个rust线程:

  • 线程 1 获取GIL
  • 线程 1 锁定互斥锁
  • 线程 1 调用 Python 解释器,释放 GIL
  • 线程2获取GIL
  • 线程 2 尝试锁定互斥锁,但被阻止
  • 线程 1 的 Python 解释器调用被阻止,开始尝试重新获取线程 2 持有的 GIL
  • 由于线程 1 目前持有互斥锁,线程 2 持有 GIL,死锁发生

为了避免死锁,你应该在尝试锁定互斥锁或异步代码中的 await 之前释放 GIL,例如使用 Python::allow_threads

6.2 python对象类型

PyAny

表示:未指定类型的 Python 对象,仅限于 GIL 生命周期中使用。目前, PyAny 只能作为不可变引用 &PyAny 使用。

使用:每当你想要引用某个 Python 对象并且在需要访问该对象的整个期间都拥有 GIL 时。例如,在 Rust 中实现的 pyfunctionpymethod 的中间值和参数,且参数允许任何类型时。

许多与 Python 对象交互的通用方法都位于 PyAny 结构体上,例如 getattrsetattr.call

类型转换:

对于 &PyAny 对象引用 any ,如果其中底层对象是 Python 原生类型,例如列表:

1
2
3
4
5
6
7
8
9
10
let obj: &PyAny = PyList::empty(py);

// To &PyList with PyAny::downcast
let _: &PyList = obj.downcast()?;

// To Py<PyAny> (aka PyObject) with .into()
let _: Py<PyAny> = obj.into();

// To Py<PyList> with PyAny::extract
let _: Py<PyList> = obj.extract()?;

对于 &PyAny 对象引用 any ,如果其中底层对象是 #[pyclass]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let obj: &PyAny = Py::new(py, MyClass {})?.into_ref(py);

// To &PyCell<MyClass> with PyAny::downcast
let _: &PyCell<MyClass> = obj.downcast()?;

// To Py<PyAny> (aka PyObject) with .into()
let _: Py<PyAny> = obj.into();

// To Py<MyClass> with PyAny::extract
let _: Py<MyClass> = obj.extract()?;

// To MyClass with PyAny::extract, if MyClass: Clone
let _: MyClass = obj.extract()?;

// To PyRef<'_, MyClass> or PyRefMut<'_, MyClass> with PyAny::extract
let _: PyRef<'_, MyClass> = obj.extract()?;
let _: PyRefMut<'_, MyClass> = obj.extract()?;

PyTuple 、 PyDict 等等

表示:已知类型的本机 Python 对象,仅限于 GIL 生命周期,就像 PyAny 一样。

使用:每当你想要在持有 GIL 的同时使用本机 Python 类型进行操作时。与 PyAny 一样,这是用于函数参数和中间值的最方便的形式。

这些类型都实现了 Deref<Target = PyAny> ,因此它们都公开了可以在 PyAny 上找到的相同方法。

要查看 PyO3 公开的所有 Python 类型,你应该查阅 pyo3::types 模块

类型转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let list = PyList::empty(py);

// Use methods from PyAny on all Python types with Deref implementation
let _ = list.repr()?;

// To &PyAny automatically with Deref implementation
let _: &PyAny = list;

// To &PyAny explicitly with .as_ref()
let _: &PyAny = list.as_ref();

// To Py<T> with .into() or Py::from()
let _: Py<PyList> = list.into();

// To PyObject with .into() or .to_object(py)
let _: PyObject = list.into();

Py<T>PyObject

表示:它们是对 Python 对象的独立于 GIL 的引用。这可以是 Python 本机类型(如 PyTuple ),也可以是 Rust 中实现的 pyclass 类型。最常用的变体 Py<PyAny> 也称为 PyObject

使用:每当你想要携带对 Python 对象的引用而不关心 GIL 生命周期时。例如,将 Python 对象引用存储在比 Python-Rust FFI 边界更长的 Rust 结构中,或者将 Rust 中实现的函数中的对象返回给 Python。

可以使用带有 .clone() 的 Python 引用计数进行克隆。

类型转换示例:

对于 Py<PyList> ,转换如下:

1
2
3
4
5
6
7
8
9
10
let list: Py<PyList> = PyList::empty(py).into();

// To &PyList with Py::as_ref() (borrows from the Py)
let _: &PyList = list.as_ref(py);

// To &PyList with Py::into_ref() (moves the pointer into PyO3's object storage)
let _: &PyList = list.into_ref(py);

// To Py<PyAny> (aka PyObject) with .into()
let _: Py<PyAny> = list.into();

对于 #[pyclass] struct MyClassPy<MyClass> 的转换如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let my_class: Py<MyClass> = Py::new(py, MyClass { })?;

// To &PyCell<MyClass> with Py::as_ref() (borrows from the Py)
let _: &PyCell<MyClass> = my_class.as_ref(py);

// To &PyCell<MyClass> with Py::into_ref() (moves the pointer into PyO3's object storage)
let _: &PyCell<MyClass> = my_class.into_ref(py);

// To Py<PyAny> (aka PyObject) with .into_py(py)
let _: Py<PyAny> = my_class.into_py(py);

// To PyRef<'_, MyClass> with Py::borrow or Py::try_borrow
let _: PyRef<'_, MyClass> = my_class.try_borrow(py)?;

// To PyRefMut<'_, MyClass> with Py::borrow_mut or Py::try_borrow_mut
let _: PyRefMut<'_, MyClass> = my_class.try_borrow_mut(py)?;

PyCell<SomeType>

表示:对包装在 Python 对象中的 Rust 对象( PyClass 的实例)的引用。cell部分类似于 stdlib 的 RefCell ,以允许访问 &mut 引用。

使用:用于访问实例的纯 Rust API(采用 &SomeType&mut SomeType 的成员和函数),同时维护 Rust 引用的别名规则。

与 PyO3 的 Python 原生类型一样, PyCell<T> 实现 Deref<Target = PyAny> ,因此它也公开 PyAny 上的所有方法。

类型转换:

PyCell<T> 可用于分别通过 PyRef<T>PyRefMut<T> 访问 &T&mut T

1
2
3
4
5
6
7
8
9
let cell: &PyCell<MyClass> = PyCell::new(py, MyClass {})?;

// To PyRef<T> with .borrow() or .try_borrow()
let py_ref: PyRef<'_, MyClass> = cell.try_borrow()?;
let _: &MyClass = &*py_ref;

// To PyRefMut<T> with .borrow_mut() or .try_borrow_mut()
let mut py_ref_mut: PyRefMut<'_, MyClass> = cell.try_borrow_mut()?;
let _: &mut MyClass = &mut *py_ref_mut;

PyCell<T> 也可以像 Python 原生类型一样进行访问。

1
2
3
4
5
6
7
8
9
10
let cell: &PyCell<MyClass> = PyCell::new(py, MyClass {})?;

// Use methods from PyAny on PyCell<T> with Deref implementation
let _ = cell.repr()?;

// To &PyAny automatically with Deref implementation
let _: &PyAny = cell;

// To &PyAny explicitly with .as_ref()
let _: &PyAny = cell.as_ref();

PyRef<SomeType>PyRefMut<SomeType>

表示: PyCell 使用的引用包装类型来跟踪借用,类似于 RefCell 使用的 RefRefMut

使用:借用 PyCell 时。它们还可以与 .extract() 一起用于 Py<T>PyAny 等类型,以快速获取引用。

二、在rust代码中运行python