Python 虽然存在易于编写的有点,但是会存在性能问题,无法应对部分高性能场景。如果我们即想兼顾Python的易用性,又想提高Python的运行效率,可以使用多种语言混合编程(Python Hybird Programming),借用C/C++/Rust等高性能语言与Python交互,来加速我们的Python程序。下面介绍Python 与 Rust 的混合编程。
要完成Python与Rust 语言的混合编程,社区比较流行的方案是 PyO3。它是 Python 的 Rust 绑定,这包括用 Rust 语言取运行 Python 代码并与之交互,以及编写原生 Python 模块。PyO3 一开始只是作为 rust-cpython 的分支出现, 后来由于 rust-cpython 缺乏维护, PyO3 开始在 Rust 社区流行, 随着时间推移 PyO3 与 rust-cpython 有了根本的差异。由于导出包使用 Rust 语言开发,以及 Rust 语言速度快且内存利用率极高,因此在运行效率及性能上优于同等 Python 实现模块。
同时社区提供了 maturin 或 setuptools-rust,来方便将你的 Rust Crate 作为 Python 模块构建、测试和发布。 您可以在 examples/word-count 中找到 setuptools-rust 的示例,而 maturin 应该无需任何配置即可在您的 crate 上使用。
Python模块
您可以按以下方式创建模块:
use pyo3::prelude::*;
// add bindings to the generated Python module
// N.B: "rust2py" must be the name of the `.so` or `.pyd` file.
/// This module is implemented in Rust.
#[pymodule]
fn rust2py(py: Python, m: &PyModule) -> PyResult<()> {
// PyO3 aware function. All of our Python interfaces could be declared in a separate module.
// Note that the `#[pyfn()]` annotation automatically converts the arguments from
// Python objects to Rust values, and the Rust return value back into a Python object.
// The `_py` argument represents that we're holding the GIL.
#[pyfn(m, "sum_as_string")]
fn sum_as_string_py(_py: Python, a: i64, b: i64) -> PyResult<String> {
let out = sum_as_string(a, b);
Ok(out)
}
Ok(())
}
// logic implemented as a normal Rust function
fn sum_as_string(a: i64, b: i64) -> String {
format!("{}", a + b)
}
#[pymodule]
过程宏属性负责将模块的初始化函数导出到 Python。 它可以将模块的名称作为参数,该名称必须是 .so
或 .pyd
文件的名称,默认值为 Rust 函数的名称。
如果模块的名称(默认名称为函数名称)与 .so
或 .pyd
文件的名称不匹配,则会在Python中收到以下错误消息,并显示导入错误:
ImportError: dynamic module does not define module export function (PyInit_name_of_your_module)
要导入模块,请按照 README 文件中的说明复制共享库,或使用工具,例如:使用 maturin 运行 maturin develop
,或者使用 setuptools-rust 运行python setup.py develop
。
文档
模块初始化功能的Rust文档注释将自动作为模块的Python文档字符串应用。
>>> import rust2py
>>> print(rust2py.__doc__)
This module is implemented in Rust.
模块作为对象
在Python中,模块是第一类对象。 这意味着您可以将它们存储为值或将它们添加到字典或其他模块中:
use pyo3::prelude::*;
use pyo3::{wrap_pyfunction, wrap_pymodule};
use pyo3::types::IntoPyDict;
#[pyfunction]
fn subfunction() -> String {
"Subfunction".to_string()
}
fn init_submodule(module: &PyModule) -> PyResult<()> {
module.add_function(wrap_pyfunction!(subfunction, module)?)?;
Ok(())
}
#[pymodule]
fn supermodule(py: Python, module: &PyModule) -> PyResult<()> {
let submod = PyModule::new(py, "submodule")?;
init_submodule(submod)?;
module.add_submodule(submod)?;
Ok(())
}
这样,您可以在单个扩展模块中创建模块层次结构。不必在嵌套模块上添加 #[pymodule]
,仅在顶层模块上才需要。
Python 函数
PyO3支持两种在Python中定义自由函数的方式。 两者都需要将功能注册到模块。
一种方法是在模块定义中定义函数,用#[pyfn]
注释。
use pyo3::prelude::*;
#[pymodule]
fn rust2py(py: Python, m: &PyModule) -> PyResult<()> {
#[pyfn(m, "sum_as_string")]
fn sum_as_string_py(_py: Python, a:i64, b:i64) -> PyResult<String> {
Ok(format!("{}", a + b))
}
Ok(())
}
另一个是使用#[pyfunction]
注释一个函数,然后使用wrap_pyfunction!
宏将其添加到模块中。
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;
#[pyfunction]
fn double(x: usize) -> usize {
x * 2
}
#[pymodule]
fn module_with_functions(py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(double, m)?).unwrap();
Ok(())
}
参数解析
#[pyfunction]
和#[pyfn]
属性都支持指定参数解析的详细信息。 有关详细信息,请参见“类”一章的“方法参数”部分。 这是一个函数示例,该函数接受任意关键字参数(Python语法中的** kwargs)并返回传递的数字:
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;
use pyo3::types::PyDict;
#[pyfunction(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
为了使函数签名可用于通过inspect.signature
检索的Python,请使用#[text_signature]
批注,如以下示例所示。/
表示仅位置参数的结尾。 (这不是该库的特别功能,而是CPython用于注释内置函数签名的通用格式。)
use pyo3::prelude::*;
/// This function adds two unsigned 64-bit integers.
#[pyfunction]
#[text_signature = "(a, b, /)"]
fn add(a: u64, b: u64) -> u64 {
a + b
}
这也适用于类和方法:
use pyo3::prelude::*;
use pyo3::types::PyType;
// it works even if the item is not documented:
#[pyclass]
#[text_signature = "(c, d, /)"]
struct MyClass {}
#[pymethods]
impl MyClass {
// the signature for the constructor is attached
// to the struct definition instead.
#[new]
fn new(c: i32, d: &str) -> Self {
Self {}
}
// the self argument should be written $self
#[text_signature = "($self, e, f)"]
fn my_method(&self, e: i32, f: i32) -> i32 {
e + f
}
#[classmethod]
#[text_signature = "(cls, e, f)"]
fn my_class_method(cls: &PyType, e: i32, f: i32) -> i32 {
e + f
}
#[staticmethod]
#[text_signature = "(e, f)"]
fn my_static_method(e: i32, f: i32) -> i32 {
e + f
}
}
请注意,在 Python 3.10 或更高版本之前,类上的 text_signature
与 abi3
模式下的编译不兼容。
闭包
当前,Rust中的 Fn
与Python中的可调用对象之间没有转换。 这绝对是可能的,而且非常有用,因此欢迎您提供帮助。 同时,您可以执行以下操作。
调用Rust中的Python函数
在Python中调用Rust函数
访问函数的模块
访问FFI函数
为了使 Rust 函数可从Python调用,PyO3生成一个函数,
extern "C" Fn(slf: *mut PyObject, args: *mut PyObject, kwargs: *mut PyObject) -> *mut Pyobject
并将对 Rust 的调用嵌入该 FFI 包装函数内部的函数。该包装器处理从输入 PyObjects
中提取常规参数和关键字参数的过程。由于此函数不是用户定义的,而是构建 PyCFunction
所必需的,因此PyO3提供了raw_pycfunction!()
宏来获取此生成的包装器的标识符。
在给定 #[pyfunction]
和 PyModule
的情况下,可以使用 wrap_pyfunction
宏直接获取 PyModule: wrap_pyfunction!(rust_fun, module)
。
Python 类
PyO3 公开了一组由Rust的proc宏系统提供支持的属性,这些属性用于将Python类定义为Rust结构。 本章将讨论它们提供的功能和配置。
定义一个类
要定义自定义Python类,需要使用#[pyclass]
属性对 Rust 结构进行注释。
#[pyclass]
struct MyClass {
num: i32,
debug: bool,
}
因为Python对象是由Python解释器在线程之间自由共享的,所以所有带#[pyclass]注释的结构体都必须实现Send
。上面的示例为MyClass
生成PyTypeInfo
,PyTypeObject
和PyClass
的实现。
将类添加到模块中
然后可以使用add_class()
将自定义 Python 类添加到模块中。
#[pymodule]
fn mymodule(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<MyClass>()?;
Ok(())
}
PyCell和内部可变性
有时需要将pyclass
转换为Python对象,并从Rust代码访问它(例如,用于测试它)。PyCell
是它的主要接口。 PyCell<T:PyClass>
总是在Python堆中分配,因此Rust没有所有权。 换句话说,Rust 代码只能提取 &PyCell<T>
,而不能提取 PyCell<T>
。因此,为了安全地改变 &PyCell
后面的数据,PyO3采用了内部可变性模式(如 RefCell
)。熟悉 RefCell
的用户可以像 RefCell
一样使用 PyCell
。
对于不太熟悉 RefCell
的用户,这里提醒一下Rust的借阅规则:
- 引用必须始终有效。
与 RefCell
一样, PyCell
通过在运行时跟踪引用来确保这些借用规则。
#[pyclass]
struct MyClass {
#[pyo3(get)]
num: i32,
debug: bool,
}
let gil = Python::acquire_gil();
let py = gil.python();
let obj = PyCell::new(py, MyClass { num: 3, debug: true }).unwrap();
{
let obj_ref = obj.borrow(); // Get PyRef
assert_eq!(obj_ref.num, 3);
// You cannot get PyRefMut unless all PyRefs are dropped
assert!(obj.try_borrow_mut().is_err());
}
{
let mut obj_mut = obj.borrow_mut(); // Get PyRefMut
obj_mut.num = 5;
// You cannot get any other refs until the PyRefMut is dropped
assert!(obj.try_borrow().is_err());
assert!(obj.try_borrow_mut().is_err());
}
// You can convert `&PyCell` to a Python object
pyo3::py_run!(py, obj, "assert obj.num == 5")
&PyCell<T>
与 GILGuard
的生命周期相同。为了延长对象的生命周期(例如,将其存储在一个 Rust 的结构中),你可以使用 Py<T>
,它存储一个比GIL生存期更长的对象,因此需要 Python<'_>
令牌来访问。
#[pyclass]
struct MyClass {
num: i32,
}
fn return_myclass() -> Py<MyClass> {
let gil = Python::acquire_gil();
let py = gil.python();
Py::new(py, MyClass { num: 1 }).unwrap()
}
let gil = Python::acquire_gil();
let obj = return_myclass();
let cell = obj.as_ref(gil.python()); // Py<MyClass>::as_ref returns &PyCell<MyClass>
let obj_ref = cell.borrow(); // Get PyRef<T>
assert_eq!(obj_ref.num, 1);
自定义类
#[pyclass]
宏接受以下参数:
name="XXX"
-设置Python代码中显示的类名。默认情况下,结构名被用作类名。freelist=XXX
- freelist参数为自定义类增加了对自由分配列表的支持。性能改进适用于通常在一行中创建和删除的类型,这样它们就可以从自由列表中受益。XXX是一些项目的免费列表。gc
-带有gc参数的类参与Python垃圾回收。 如果自定义类包含对可以收集的其他Python对象的引用,则必须实现PyGCProtocol特性。weakref
-添加对Python弱引用的支持。extends=BaseType
-使用自定义基类。基类基类型必须实现PyTypeInfo
。subclass
-允许Python类从这个类继承。dict
-增加__dict__
支持,这样该类的实例就有一个包含任意实例变量的字典。unsendable
可以安全地将!Send结构公开给Python,所有对象都可以由多个线程访问。 当被另一个线程访问时,标有不可发送的紧急消息的类。module="XXX"
-设置将在其中显示类的模块的名称。如果未指定,则该类将是内置模块的虚拟成员。构造函数
```rust[pyclass]
struct MyClass { num: i32, }
[pymethods]
impl MyClass {
#[new]
fn new(num: i32) -> Self {
MyClass { num }
}
}
<a name="EpdHa"></a>
### 返回类型
<a name="zwNNR"></a>
### 继承
<a name="iCXah"></a>
### 对象属性
<a name="bRRWS"></a>
### 实例方法
<a name="18wAL"></a>
### 类方法
<a name="v9U1A"></a>
### 静态方法
<a name="IWuaa"></a>
## 类型转换
在本部分中,我们将讨论Python类型到PyO3提供的Rust类型的映射,以及在它们之间执行转换所需的特征。
<a name="XBtty"></a>
### 将Rust类型映射到Python类型
在编写可从Python调用的函数时(例如`#[pyfunction]`或`#[pymethods]`块),函数参数需要特征`FromPyObject`,函数返回值需要`IntoPy<PyObject>`。<br />请查阅下一节中的表,以查找实现这些特征的PyO3提供的Rust类型。
<a name="EJOJc"></a>
#### 参数类型
接受函数参数时,可以使用 Rust 库类型或 PyO3 的Python原生类型。 (有关如何使用它们的讨论,请参见下一节。)<br />下表包含Python类型和将接受它们的相应函数参数类型:
| **Python** | **Rust** | **Rust (Python-native)** |
| --- | --- | --- |
| `object` | - | `&PyAny` |
| `str` | `String`, `Cow<str>`, `&str` | `&PyUnicode` |
| `bytes` | `Vec<u8>`, `&[u8]` | `&PyBytes` |
| `bool` | `bool` | `&PyBool` |
| `int` | Any integer type (`i32`, `u32`, `usize`, etc) | `&PyLong` |
| `float` | `f32`, `f64` | `&PyFloat` |
| `complex` | `num_complex::Complex`[1](https://pyo3.rs/v0.13.2/conversions/tables.html#1) | `&PyComplex` |
| `list[T]` | `Vec<T>` | `&PyList` |
| `dict[K, V]` | `HashMap<K, V>`, `BTreeMap<K, V>`, `hashbrown::HashMap<K, V>`[2](https://pyo3.rs/v0.13.2/conversions/tables.html#2) | `&PyDict` |
| `tuple[T, U]` | `(T, U)`, `Vec<T>` | `&PyTuple` |
| `set[T]` | `HashSet<T>`, `BTreeSet<T>`, `hashbrown::HashSet<T>`[2](https://pyo3.rs/v0.13.2/conversions/tables.html#2) | `&PySet` |
| `frozenset[T]` | `HashSet<T>`, `BTreeSet<T>`, `hashbrown::HashSet<T>`[2](https://pyo3.rs/v0.13.2/conversions/tables.html#2) | `&PyFrozenSet` |
| `bytearray` | `Vec<u8>` | `&PyByteArray` |
| `slice` | - | `&PySlice` |
| `type` | - | `&PyType` |
| `module` | - | `&PyModule` |
| `datetime.datetime` | - | `&PyDateTime` |
| `datetime.date` | - | `&PyDate` |
| `datetime.time` | - | `&PyTime` |
| `datetime.tzinfo` | - | `&PyTzInfo` |
| `datetime.timedelta` | - | `&PyDelta` |
| `typing.Optional[T]` | `Option<T>` | - |
| `typing.Sequence[T]` | `Vec<T>` | `&PySequence` |
| `typing.Iterator[Any]` | - | `&PyIterator` |
| `typing.Union[...]` | See [`#[derive(FromPyObject)]`](https://pyo3.rs/v0.13.2/conversions/traits.html#deriving-a-hrefhttpsdocsrspyo3latestpyo3conversiontraitfrompyobjecthtmlfrompyobjecta-for-enums) | - |
还有一些与 GIL 和Rust定义的`#[pyclass]` 等相关的特殊类型,可能会有用:
| **What** | **Description** |
| --- | --- |
| `Python` | A GIL token, used to pass to PyO3 constructors to prove ownership of the GIL |
| `Py<T>` | A Python object isolated from the GIL lifetime. This can be sent to other threads. |
| `PyObject` | An alias for `Py<PyAny>` |
| `&PyCell<T>` | A `#[pyclass]` value owned by Python. |
| `PyRef<T>` | A `#[pyclass]` borrowed immutably. |
| `PyRefMut<T>` | A `#[pyclass]` borrowed mutably. |
要了解接受`#[pyclass]`值作为函数参数的更多细节,请参阅本Python类指南的章节。
<a name="b2VZq"></a>
#### 将Rust值返回给Python
在可从Python 调用的函数返回值时,可以以零成本使用 Python原生类型(`&PyAny`, `&PyDict`等)。<br />由于这些类型是引用,因此在某些情况下,Rust 编译器可能会要求提供生命周期注释。 如果是这种情况,则应改用`Py<PyAny>`, `Py<PyDict>`等-也是零成本的。 对于所有这些Python原生类型`T` 可以使用 `.into()` 转换从`T`创建 `Py<T>` 。<br />如果您的函数容易出错,则应返回 `PyResult<T>` 或 `Result<T, E>`,其中`E`为`From<E> for PyErr`。 如果返回 `Err` 变量,则将引发Python异常。<br />最后,以下Rust类型也可以转换为Python作为返回值:
| **Rust type** | **Resulting Python Type** |
| --- | --- |
| `String` | `str` |
| `&str` | `str` |
| `bool` | `bool` |
| Any integer type (`i32`, `u32`, `usize`, etc) | `int` |
| `f32`, `f64` | `float` |
| `Option<T>` | `Optional[T]` |
| `(T, U)` | `Tuple[T, U]` |
| `Vec<T>` | `List[T]` |
| `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` |
<a name="NB75x"></a>
## 转化 Trait
PyO3提供了一些方便的特性,可以在Python类型和Rust类型之间进行转换。
<a name="apruA"></a>
### `.extract()` 和 `FromPyObject` trait
将Python对象转换为Rust值的最简单方法是使用`.extract()`。 如果转换失败,它将返回带有类型错误的 `PyResult`,因此通常您会使用类似:
```rust
let v: Vec<i32> = obj.extract()?;
此方法可用于许多Python对象类型,并且可以生成多种Rust类型,您可以在 FromPyObject
的实现者列表中进行检查。FromPyObject
也可以用于包装为Python对象的Rust类型(参见关于类的章节)。在这里,为了既能够在可变引用上操作,又满足Rust的非混淆可变引用规则,必须提取PyO3引用包装器PyRef
和PyRefMut
。它们的工作方式类似于std::cell::RefCell
的引用包装器,并确保(在运行时)允许Rust借用。
Python异常处理
定义一个新的异常
您可以使用create_exception!
宏来定义新的异常类型:
use pyo3::create_exception;
create_exception!(module, MyError, pyo3::exceptions::PyException);
module
是包含模块的名称。MyError
是新异常类型的名称。
例如:
use pyo3::prelude::*;
use pyo3::create_exception;
use pyo3::types::IntoPyDict;
use pyo3::exceptions::PyException;
create_exception!(mymodule, CustomError, PyException);
fn main() {
let gil = Python::acquire_gil();
let py = gil.python();
let ctx = [("CustomError", py.get_type::<CustomError>())].into_py_dict(py);
py.run("assert str(CustomError) == \"<class 'mymodule.CustomError'>\"", None, Some(&ctx)).unwrap();
py.run("assert CustomError('oops').args == ('oops',)", None, Some(&ctx)).unwrap();
}
使用PyO3创建扩展模块时,您可以像这样向模块添加新的异常,以便可以从Python导入它:
create_exception!(mymodule, CustomError, PyException);
#[pymodule]
fn mymodule(py: Python, m: &PyModule) -> PyResult<()> {
// ... other elements added to module ...
m.add("CustomError", py.get_type::<CustomError>())?;
Ok(())
}
抛出异常
要引发异常,首先需要获取异常类型并构造一个新的PyErr
,然后调用PyErr::restore
方法将异常写回到 Python 解释器的全局状态。
use pyo3::{Python, PyErr};
use pyo3::exceptions::PyTypeError;
fn main() {
let gil = Python::acquire_gil();
let py = gil.python();
PyTypeError::new_err("Error").restore(py);
assert!(PyErr::occurred(py));
drop(PyErr::fetch(py));
}
检查异常类型
处理Rust错误
使用Python代码中定义的异常
use pyo3::prelude::*;
mod io {
pyo3::import_exception!(io, UnsupportedOperation);
}
fn tell(file: &PyAny) -> PyResult<u64> {
use pyo3::exceptions::*;
match file.call_method0("tell") {
Err(_) => Err(io::UnsupportedOperation::new_err("not supported: tell")),
Ok(x) => x.extract::<u64>(),
}
}