利用pyo3加速python
pyo3:https://github.com/PyO3/pyo3
maturin:https://github.com/PyO3/maturin
两种方式:
- 在python代码中调用rust编写的模块
- 在rust代码中运行python
一、在python代码中调用rust编写的模块
PyO3 可用于生成本机 Python 模块。最简单的方法是使用 maturin。maturin 是一个用很少的配置可以构建和发布基于 Rust 的 Python 包的工具。通过以下步骤安装 maturin ,使用它生成并构建新的 Python 包,然后可以运行 Python 导入并执行包中的函数。
1 一个简单的示例
1.1 虚拟环境创建和maturin安装
首先,按照以下命令创建一个包含新 Python virtualenv 的新目录,并使用 Python 的包管理器 pip 将 maturin 安装到 virtualenv 中:
| 1 | mkdir string_sum | 
在这个 string_sum 目录中,现在运行 maturin init 。这将生成新的包源。当命令行提示选择要使用的绑定时,选择 pyo3 绑定:
| 1 | maturin init | 
此命令生成的最重要的文件是 Cargo.toml 和 lib.rs ,它们大致如下所示:
Cargo.toml
| 1 | [package] | 
src/lib.rs
| 1 | use pyo3::prelude::*; | 
1.2 编译并在python中运行
最后,运行 maturin develop 。这将构建包并将其安装到之前创建和激活的 Python virtualenv 中。然后该包就可以从 python 中使用了:
| 1 | maturin develop | 
要对包进行更改,只需编辑 Rust 源代码,然后重新运行 maturin develop 进行重新编译。注意,只要rust代码被更改,就要重新编译。
2 python模块创建
现在,让我们看看刚才自动生成的代码,并逐渐介绍其用法。
| 1 | use pyo3::prelude::*; | 
在这里,我们通过 #[pymodule] 过程宏创建模块,它负责将模块的初始化函数导出到 Python。模块的名称默认这里 Rust 函数的名称。你可以使用 #[pyo3(name = "custom_name")] 覆盖模块名称:
| 1 | 
 | 
注意,如果修改了模块名称,也要同时修改Cargo.toml中lib.name,使它们相匹配,否则python无法导入该模块。
| 1 | [lib] | 
2.1 子模块创建
可以使用 PyModule.add_submodule() 在单个扩展模块中创建模块层次结构。例如,可以定义模块 parent_module 和 parent_module.child_module 。
| 1 | use pyo3::prelude::*; | 
这里注意,此处并没有定义python package,因此python代码不允许使用 from parent_module import child_module 直接导入子模块。有关更多信息,请参阅 #759 和 #1517.
嵌套模块上不需要添加 #[pymodule] ,仅在顶层模块上需要添加 #[pymodule] 。
3 python函数创建
#[pyfunction] 属性用于从 Rust 函数定义 Python 函数。定义后,需要使用 wrap_pyfunction! 宏将该函数添加到模块中。比如之前默认生成的代码:
| 1 | use pyo3::prelude::*; | 
这段代码在名为 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::*; 
 fn pyfunction_with_module(module: &PyModule) -> PyResult<&str> {
 module.name()
 }
 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)
 }
 fn object_length( argument: usize) -> usize {
 argument
 }
3.2 #[pyfn] 简写
#[pyfunction] 和 wrap_pymodule! 有一个简写:函数可以放置在模块定义中并用 #[pyfn] 注释。为了简化 PyO3,预计 #[pyfn] 可能会在未来版本中删除(参见#694)。
#[pyfn] 的示例如下:
| 1 | use pyo3::prelude::*; | 
#[pyfn(m)] 只是 #[pyfunction] 的语法糖,上面的代码扩展为以下内容:
| 1 | use pyo3::prelude::*; | 
3.3 函数签名
#[pyfunction] 属性还接受参数来控制生成的 Python 函数如何接受参数。就像在 Python 中一样,参数可以是仅限位置的、仅限关键字的,或者接受其中任何一个。 *args 列表和 **kwargs 字典也可以被接受。这些参数也适用于 #[pymethods] ,它将在后面进行介绍。
与 Python 一样,默认情况下 PyO3 接受所有参数作为位置参数或关键字参数。默认情况下,大多数参数都是必需的,但尾随 Option<_> 参数除外,这些参数隐式指定了默认值 None 。有两种方法可以修改此行为:
有两种方法可以修改此行为:
- #[pyo3(signature = (...))]选项允许使用 Python 语法编写签名。
- 额外的参数直接传递给 #[pyfunction]。(已弃用)
使用 #[pyo3(signature = (…))]
例如,下面是一个接受任意关键字参数(Python 语法中的 **kwargs )并返回传递的数字的函数:
| 1 | use pyo3::prelude::*; | 
就像在 Python 中一样,以下构造可以是签名的一部分:
- /:仅位置参数分隔符,- /之前定义的每个参数都是仅位置参数。
- *:var 参数分隔符,- *之后定义的每个参数都是仅关键字参数。
- *args:“args”是 var args。- args参数的类型必须是- &PyTuple。
- **kwargs:“kwargs”接收关键字参数。- kwargs参数的类型必须是- Option<&PyDict>。
- arg=Value:具有默认值的参数。如果- arg参数在 var 参数之后定义,则它被视为仅关键字参数。请注意,- Value必须是有效的 Rust 代码,PyO3 只是将其插入到生成的代码中而不进行修改。
例子:
| 1 | use pyo3::types::{PyDict, PyTuple}; | 
Python 类型的参数不能作为签名的一部分:
| 1 | 
 | 
注意: / 和 * 参数(如果包含)的位置控制处理位置和关键字参数的系统。在Python中:
| 1 | import mymodule | 
输出:
| 1 | py_args=('World', 666), py_kwargs=Some({'x': 44, 'y': 55}), name=Hello, num=44 | 
要使用像
struct这样的rust关键字作为函数参数,请在签名和函数定义中使用“原始标识符”语法r#struct:
2
3
4
fn function_with_keyword(r#struct: &str) {
/* ... */
}
尾随可选参数
为了方便起见,没有 #[pyo3(signature = (...))] 选项的函数会将尾随 Option<T> 参数视为具有默认值 None 。在下面的示例中,PyO3 将创建带有 increment(x, amount=None) 签名的 increment 。
| 1 | use pyo3::prelude::*; | 
要使尾随 Option<T> 参数成为必需,但仍接受 None ,需要添加 #[pyo3(signature = (...))] 注释。对于上面的示例,这将是 #[pyo3(signature = (x, amount))] :
| 1 | 
 | 
为了避免混淆,当 Option<T> 参数被不是 Option<T> 的参数包围时,PyO3 需要 #[pyo3(signature = (...))] 。
使函数签名可供 Python 使用
函数签名通过 __text_signature__ 属性向 Python 公开。 PyO3 直接从 Rust 函数自动为每个 #[pyfunction] 和所有 #[pymethods] 生成此值,同时考虑使用 #[pyo3(signature = (...))] 选项完成的任何覆盖。
此自动生成只能显示字符串、整数、布尔类型和 None 的默认参数值。任何其他默认参数将显示为 ... 。 ( .pyi 类型存根文件通常也以相同的方式使用 ... 作为默认参数。)
如果需要调整自动生成的签名,可以使用 #[pyo3(text_signature)] 选项覆盖它。)
下面的示例创建一个函数 add ,它接受两个仅位置参数 a 和 b ,其中 b 的默认值为零。
| 1 | use pyo3::prelude::*; | 
以下从 IPython 输出演示了如何从 Python 工具中看到生成的签名:
| 1 | >>> pyo3_test.add.__text_signature__ | 
覆盖生成的签名
使用#[pyo3(text_signature = "(<some signature>)")] 属性可用于覆盖默认生成的签名。
在下面的代码片段中,文本签名属性用于包含参数 b 的默认值 0 ,而不是自动生成的默认值 ... :
| 1 | use pyo3::prelude::*; | 
PyO3 将包含未修改的注释内容作为 __text_signature 。下面显示了 IPython 现在将如何呈现此内容(b 的默认值 0):
| 1 | >>> pyo3_test.add.__text_signature__ | 
如果根本不需要签名, #[pyo3(text_signature = None)] 将禁用内置签名。下面的代码片段演示了它的用法:
| 1 | use pyo3::prelude::*; | 
现在函数的 __text_signature__ 将被设置为 None ,并且 IPython 将不会在帮助中显示任何签名:
| 1 | >>> pyo3_test.add.__text_signature__ == None | 
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 异常时,异常将存储在 PyResult的Err中。
- 然后,通过 Rust 代码传递 Python 异常会使用所有“正常”技术,例如 ?运算符,并以PyErr作为错误类型。
- 最后,当 PyResult通过 PyO3 从 Rust 跨回到 Python 时,如果结果是Err,则将引发包含的异常。
至于Rust部分的异常处理(比如?)等相关内容,我们假设你已经了解。
从函数中引发异常
如上所述,当包含 Err 的 PyResult 从 Rust 跨越到 Python 时,PyO3 将引发其中包含的异常。
因此,要从 #[pyfunction] 引发异常,需要将返回类型 T 更改为 PyResult<T> 。当函数返回 Err 时,它将引发 Python 异常。 (只要错误 E 具有 PyErr 的 From 转换,就可以使用其他 Result<T, E> 类型 )
这也适用于 #[pymethods] 中的函数。
例如,当输入为负数时,以下 check_positive 函数会引发 ValueError :
| 1 | use pyo3::exceptions::PyValueError; | 
所有内置的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 | use std::num::ParseIntError; | 
当传递一个不包含浮点数的字符串时,引发的异常将如下所示:
| 1 | parse_int("bar") | 
作为更完整的示例,以下代码片段定义了一个名为 CustomIOError 的 Rust 错误。然后它定义一个 From<CustomIOError> for PyErr ,它返回一个代表 Python 的 OSError 的 PyErr 。因此,它可以直接在 #[pyfunction] 的结果中使用此错误,如果必须将其传播到 Python 异常中,则依赖于转换。
| 1 | use pyo3::exceptions::PyOSError; | 
如果需要延迟构造 Python 异常实例,则可以实现 PyErrArguments 特征而不是 From 。在这种情况下,实际的异常参数创建会被延迟,直到需要 PyErr 为止。 最后要注意的是,任何具有 From 转换的错误 E 都可以与 ? (“try”)运算符一起使用。上述 parse_int 的替代实现如下,它返回 PyResult :
| 1 | use pyo3::prelude::*; | 
第三方 Rust 错误类型
Rust 编译器不允许在定义类型的包之外实现类型的特征,这被称为“孤儿规则”。
给定在第三方代码中定义的类型 OtherError ,有两种主要策略可将其与 PyO3 集成:
- 创建一个新类型包装器,例如 MyOtherError。然后实现From<MyOtherError> for PyErr(或PyErrArguments),以及MyOtherError的From<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 | use pyo3::prelude::*; | 
4 python类
PyO3 公开了一组由 Rust 的 proc 宏系统支持的属性,用于将 Python 类定义为 Rust 结构。
主要属性是 #[pyclass] ,它被放置在 Rust struct 或 enum (类似 C 的枚举类型)上以为其生成 Python 类型。它们通常还会有一个带 #[pymethods] 注释的 impl 结构块,用于为生成的 Python 类型定义 Python 方法和常量。 (如果启用 multiple-pymethods features,则允许每个 #[pyclass] 具有多个 #[pymethods] 块。) #[pymethods] 也可能具有 Python 实现魔法方法,例如 __str__ 。
4.1 定义一个新类
要定义自定义 Python 类,将 #[pyclass] 属性添加到 Rust 结构体或枚举。
| 1 | use pyo3::prelude::*; | 
上面的示例为 MyClass 和 MyEnum 生成 PyTypeInfo 和 PyClass 的实现。
一些限制
为了将 Rust 类型与 Python 集成,PyO3 需要对可以使用 #[pyclass] 的类型进行一些限制。特别是,它们必须没有生命周期参数,没有通用参数,并且必须实现 Send 。下面解释每一个的原因。
没有生命周期参数
Rust 编译器使用 Rust 生命周期来推断程序的内存安全性。它们只是编译时的概念;无法从像 Python 这样的动态语言在运行时访问 Rust 生命周期。
一旦 Rust 数据暴露给 Python,Rust 编译器就无法保证数据的存活时间。 Python 是一种引用计数语言,这些引用可以保留任意长的时间,而 Rust 编译器无法追踪这些引用。正确表达这一点的唯一可能的方法是要求任何 #[pyclass] 都不会在短于 'static 生命周期的任何生命周期内借用数据,即 #[pyclass] 不能有任何生命周期参数。
当需要在 Python 和 Rust 之间共享数据所有权时,不要使用具有生命周期的借用引用,而是考虑使用引用计数智能指针,例如 Arc 或 Py 。
无泛型参数
带有泛型参数 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 | 
 | 
如果你的 new 方法可能失败,可以返回 PyResult<Self> 。
| 1 | 
 | 
如果你想返回现有对象(例如,因为你的 new 方法缓存了它返回的值), new 可以返回 pyo3::Py<Self> 。
在此处,Rust 方法名称并不重要,因此你仍然可以使用 new() 作为 Rust 级别的构造函数。
如果没有声明带有 #[new] 标记的方法,则只能从 Rust 创建对象实例,而不能从 Python 创建对象实例。
4.3 将类添加到模块中
下面就是创建模块并向其中添加我们的类:
| 1 | 
 | 
4.4 自定义类
#[pyclass] 可以与以下参数一起使用:
| 参数 | 描述 | 
|---|---|
| crate = "some::path" | 导入 pyo3crate的路径(如果无法在::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 | // Argument supplied directly to the `#[pyclass]` annotation. | 
4.5 类的继承和更高级用法
有关更多类的继承和高级用法,请参考官方文档。
5 类型转换
当编写可从 Python 调用的函数(例如 #[pyfunction] 或 #[pymethods] 块中)时,函数参数需要实现 FromPyObject 特征,返回值需要实现IntoPy<PyObject>特征。
PyO3默认已经为部分rust类型实现了这些特征,它们对应python的类型。我们将在下文中的表格里展示。
5.1 接受函数参数
当接受函数参数时,可以使用 Rust 库类型或 PyO3 提供的 Python 原生类型。
下表包含 Python 类型以及接受它们的相应函数参数类型:
| Python | Rust | Rust (Python 原生类型) | 
|---|---|---|
| object | - | &PyAny | 
| str | String,Cow<str>,&str,OsString,PathBuf,Path | &PyString,&PyUnicode | 
| bytes | Vec<u8>,&[u8],Cow<[u8]> | &PyBytes | 
| bool | bool | &PyBool | 
| int | i8,u8,i16,u16,i32,u32,i64,u64,i128,u128,isize,usize,num_bigint::BigInt1,num_bigint::BigUint1 | &PyLong | 
| float | f32,f64 | &PyFloat | 
| complex | num_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 | 
| bytearray | Vec<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.Decimal | rust_decimal::Decimal5 | - | 
| ipaddress.IPv4Address | std::net::IpAddr,std::net::IpV4Addr | - | 
| ipaddress.IPv6Address | std::net::IpAddr,std::net::IpV6Addr | - | 
| os.PathLike | PathBuf,Path | &PyString,&PyUnicode | 
| pathlib.Path | PathBuf,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] 相关的特殊类型:
| 类型 | 描述 | 
|---|---|
| Python | GIL 令牌,用于传递给 PyO3 构造函数以证明 GIL 的所有权 | 
| Py<T> | 与 GIL 生命周期隔离的 Python 对象。可以发送到其他线程。 | 
| PyObject | Py<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>,它只接受包含整数的 Pythonlist。 Python 原生的等价物&PyList则会接受包含任何类型的 Python 对象的 Pythonlist。
对于大多数 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 类型 | 
|---|---|
| String | str | 
| &str | str | 
| bool | bool | 
| 任何数字类型 ( i32,u32,usize, 等) | int | 
| f32,f64 | float | 
| 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 引用包装器 PyRef 和 PyRefMut 。它们的工作方式类似于 std::cell::RefCell 的引用包装器,并确保(在运行时)允许 Rust 借用。
派生 FromPyObject
如果成员类型本身实现 FromPyObject ,则可以自动派生多种结构体和枚举的 FromPyObject 。这甚至包括具有泛型类型 T: FromPyObject 的成员。但注意,不支持空的枚举、枚举变体和结构体的派生。
为结构体派生 FromPyObject
派生生成的代码将尝试访问 Python 对象上的属性 my_string (即 obj.getattr("my_string") ),并调用该属性上的 extract() 。
| 1 | use pyo3::prelude::*; | 
通过在字段上设置 #[pyo3(item)] 属性,PyO3 将尝试通过调用 Python 对象上的 get_item 方法来提取值。
| 1 | use pyo3::prelude::*; | 
传递给 getattr 和 get_item 的参数也可以配置:
| 1 | use pyo3::prelude::*; | 
这尝试从属性 name 中提取 string_attr ,并从具有键 "key" 的映射中提取 string_in_mapping 。 attribute 的参数仅限于非空字符串文字,而 item 可以采用任何实现 ToBorrowedObject 的有效文字。
可以在结构上使用 #[pyo3(from_item_all)] 来使用 get_item 方法提取每个字段。在这种情况下,你不能在任何字段上使用 #[pyo3(attribute)] 或几乎不使用 #[pyo3(item)] 。但是,仍然允许使用 #[pyo3(item("key"))] 指定字段的键。
| 1 | use pyo3::prelude::*; | 
更多的派生方法
更多的派生方法,请参阅这里。
IntoPy<T>
此特征定义了 Rust 类型到 python 的转换。它通常实现为 IntoPy<PyObject> ,这是从 #[pyfunction] 和 #[pymethods] 返回值所需的特征。
PyO3 中的所有类型都实现此特征,就像不使用 extends 的 #[pyclass] 一样。
有时,你可能会选择为映射到 Python 类型但没有唯一 Python 类型的自定义类型实现此功能。
| 1 | use pyo3::prelude::*; | 
ToPyObject特征
ToPyObject 是一个转换特征,允许将各种对象转换为 PyObject 。 IntoPy<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 | `Python` exists |=====================================| | 
当尝试在持有 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 中实现的 pyfunction 或 pymethod 的中间值和参数,且参数允许任何类型时。
许多与 Python 对象交互的通用方法都位于 PyAny 结构体上,例如 getattr 、 setattr 和 .call 。
类型转换:
对于 &PyAny 对象引用 any ,如果其中底层对象是 Python 原生类型,例如列表:
| 1 | let obj: &PyAny = PyList::empty(py); | 
对于 &PyAny 对象引用 any ,如果其中底层对象是 #[pyclass] :
| 1 | let obj: &PyAny = Py::new(py, MyClass {})?.into_ref(py); | 
PyTuple 、 PyDict 等等
表示:已知类型的本机 Python 对象,仅限于 GIL 生命周期,就像 PyAny 一样。
使用:每当你想要在持有 GIL 的同时使用本机 Python 类型进行操作时。与 PyAny 一样,这是用于函数参数和中间值的最方便的形式。
这些类型都实现了 Deref<Target = PyAny> ,因此它们都公开了可以在 PyAny 上找到的相同方法。
要查看 PyO3 公开的所有 Python 类型,你应该查阅 pyo3::types 模块。
类型转换:
| 1 | let list = PyList::empty(py); | 
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 | let list: Py<PyList> = PyList::empty(py).into(); | 
对于 #[pyclass] struct MyClass , Py<MyClass> 的转换如下:
| 1 | let my_class: Py<MyClass> = Py::new(py, MyClass { })?; | 
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 | let cell: &PyCell<MyClass> = PyCell::new(py, MyClass {})?; | 
PyCell<T> 也可以像 Python 原生类型一样进行访问。
| 1 | let cell: &PyCell<MyClass> = PyCell::new(py, MyClass {})?; | 
PyRef<SomeType> 和 PyRefMut<SomeType>
表示: PyCell 使用的引用包装类型来跟踪借用,类似于 RefCell 使用的 Ref 和 RefMut 。
使用:借用 PyCell 时。它们还可以与 .extract() 一起用于 Py<T> 和 PyAny 等类型,以快速获取引用。
二、在rust代码中运行python
