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 functionfn 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 PyRefassert_eq!(obj_ref.num, 3);// You cannot get PyRefMut unless all PyRefs are droppedassert!(obj.try_borrow_mut().is_err());}{let mut obj_mut = obj.borrow_mut(); // Get PyRefMutobj_mut.num = 5;// You cannot get any other refs until the PyRefMut is droppedassert!(obj.try_borrow().is_err());assert!(obj.try_borrow_mut().is_err());}// You can convert `&PyCell` to a Python objectpyo3::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>## 转化 TraitPyO3提供了一些方便的特性,可以在Python类型和Rust类型之间进行转换。<a name="apruA"></a>### `.extract()` 和 `FromPyObject` trait将Python对象转换为Rust值的最简单方法是使用`.extract()`。 如果转换失败,它将返回带有类型错误的 `PyResult`,因此通常您会使用类似:```rustlet 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>(),}}
