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 实现模块。
同时社区提供了 maturinsetuptools-rust,来方便将你的 Rust Crate 作为 Python 模块构建、测试和发布。 您可以在 examples/word-count 中找到 setuptools-rust 的示例,而 maturin 应该无需任何配置即可在您的 crate 上使用。

Python模块

您可以按以下方式创建模块:

  1. use pyo3::prelude::*;
  2. // add bindings to the generated Python module
  3. // N.B: "rust2py" must be the name of the `.so` or `.pyd` file.
  4. /// This module is implemented in Rust.
  5. #[pymodule]
  6. fn rust2py(py: Python, m: &PyModule) -> PyResult<()> {
  7. // PyO3 aware function. All of our Python interfaces could be declared in a separate module.
  8. // Note that the `#[pyfn()]` annotation automatically converts the arguments from
  9. // Python objects to Rust values, and the Rust return value back into a Python object.
  10. // The `_py` argument represents that we're holding the GIL.
  11. #[pyfn(m, "sum_as_string")]
  12. fn sum_as_string_py(_py: Python, a: i64, b: i64) -> PyResult<String> {
  13. let out = sum_as_string(a, b);
  14. Ok(out)
  15. }
  16. Ok(())
  17. }
  18. // logic implemented as a normal Rust function
  19. fn sum_as_string(a: i64, b: i64) -> String {
  20. format!("{}", a + b)
  21. }

#[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文档字符串应用。

  1. >>> import rust2py
  2. >>> print(rust2py.__doc__)
  3. This module is implemented in Rust.

模块作为对象

在Python中,模块是第一类对象。 这意味着您可以将它们存储为值或将它们添加到字典或其他模块中:

  1. use pyo3::prelude::*;
  2. use pyo3::{wrap_pyfunction, wrap_pymodule};
  3. use pyo3::types::IntoPyDict;
  4. #[pyfunction]
  5. fn subfunction() -> String {
  6. "Subfunction".to_string()
  7. }
  8. fn init_submodule(module: &PyModule) -> PyResult<()> {
  9. module.add_function(wrap_pyfunction!(subfunction, module)?)?;
  10. Ok(())
  11. }
  12. #[pymodule]
  13. fn supermodule(py: Python, module: &PyModule) -> PyResult<()> {
  14. let submod = PyModule::new(py, "submodule")?;
  15. init_submodule(submod)?;
  16. module.add_submodule(submod)?;
  17. Ok(())
  18. }

这样,您可以在单个扩展模块中创建模块层次结构。不必在嵌套模块上添加 #[pymodule],仅在顶层模块上才需要。

Python 函数

PyO3支持两种在Python中定义自由函数的方式。 两者都需要将功能注册到模块。
一种方法是在模块定义中定义函数,用#[pyfn]注释。

  1. use pyo3::prelude::*;
  2. #[pymodule]
  3. fn rust2py(py: Python, m: &PyModule) -> PyResult<()> {
  4. #[pyfn(m, "sum_as_string")]
  5. fn sum_as_string_py(_py: Python, a:i64, b:i64) -> PyResult<String> {
  6. Ok(format!("{}", a + b))
  7. }
  8. Ok(())
  9. }

另一个是使用#[pyfunction]注释一个函数,然后使用wrap_pyfunction! 宏将其添加到模块中。

  1. use pyo3::prelude::*;
  2. use pyo3::wrap_pyfunction;
  3. #[pyfunction]
  4. fn double(x: usize) -> usize {
  5. x * 2
  6. }
  7. #[pymodule]
  8. fn module_with_functions(py: Python, m: &PyModule) -> PyResult<()> {
  9. m.add_function(wrap_pyfunction!(double, m)?).unwrap();
  10. Ok(())
  11. }

参数解析

#[pyfunction]#[pyfn]属性都支持指定参数解析的详细信息。 有关详细信息,请参见“类”一章的“方法参数”部分。 这是一个函数示例,该函数接受任意关键字参数(Python语法中的** kwargs)并返回传递的数字:

  1. use pyo3::prelude::*;
  2. use pyo3::wrap_pyfunction;
  3. use pyo3::types::PyDict;
  4. #[pyfunction(kwds="**")]
  5. fn num_kwds(kwds: Option<&PyDict>) -> usize {
  6. kwds.map_or(0, |dict| dict.len())
  7. }
  8. #[pymodule]
  9. fn module_with_functions(py: Python, m: &PyModule) -> PyResult<()> {
  10. m.add_function(wrap_pyfunction!(num_kwds, m)?).unwrap();
  11. Ok(())
  12. }

使函数签名可用于Python

为了使函数签名可用于通过inspect.signature检索的Python,请使用#[text_signature]批注,如以下示例所示。/表示仅位置参数的结尾。 (这不是该库的特别功能,而是CPython用于注释内置函数签名的通用格式。)

  1. use pyo3::prelude::*;
  2. /// This function adds two unsigned 64-bit integers.
  3. #[pyfunction]
  4. #[text_signature = "(a, b, /)"]
  5. fn add(a: u64, b: u64) -> u64 {
  6. a + b
  7. }

这也适用于类和方法:

  1. use pyo3::prelude::*;
  2. use pyo3::types::PyType;
  3. // it works even if the item is not documented:
  4. #[pyclass]
  5. #[text_signature = "(c, d, /)"]
  6. struct MyClass {}
  7. #[pymethods]
  8. impl MyClass {
  9. // the signature for the constructor is attached
  10. // to the struct definition instead.
  11. #[new]
  12. fn new(c: i32, d: &str) -> Self {
  13. Self {}
  14. }
  15. // the self argument should be written $self
  16. #[text_signature = "($self, e, f)"]
  17. fn my_method(&self, e: i32, f: i32) -> i32 {
  18. e + f
  19. }
  20. #[classmethod]
  21. #[text_signature = "(cls, e, f)"]
  22. fn my_class_method(cls: &PyType, e: i32, f: i32) -> i32 {
  23. e + f
  24. }
  25. #[staticmethod]
  26. #[text_signature = "(e, f)"]
  27. fn my_static_method(e: i32, f: i32) -> i32 {
  28. e + f
  29. }
  30. }

请注意,在 Python 3.10 或更高版本之前,类上的 text_signatureabi3 模式下的编译不兼容。

闭包

当前,Rust中的 Fn 与Python中的可调用对象之间没有转换。 这绝对是可能的,而且非常有用,因此欢迎您提供帮助。 同时,您可以执行以下操作。

调用Rust中的Python函数

在Python中调用Rust函数

访问函数的模块

访问FFI函数

为了使 Rust 函数可从Python调用,PyO3生成一个函数,

  1. 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 结构进行注释。

  1. #[pyclass]
  2. struct MyClass {
  3. num: i32,
  4. debug: bool,
  5. }

因为Python对象是由Python解释器在线程之间自由共享的,所以所有带#[pyclass]注释的结构体都必须实现Send。上面的示例为MyClass生成PyTypeInfoPyTypeObjectPyClass的实现。

将类添加到模块中

然后可以使用add_class()将自定义 Python 类添加到模块中。

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

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 通过在运行时跟踪引用来确保这些借用规则。

  1. #[pyclass]
  2. struct MyClass {
  3. #[pyo3(get)]
  4. num: i32,
  5. debug: bool,
  6. }
  7. let gil = Python::acquire_gil();
  8. let py = gil.python();
  9. let obj = PyCell::new(py, MyClass { num: 3, debug: true }).unwrap();
  10. {
  11. let obj_ref = obj.borrow(); // Get PyRef
  12. assert_eq!(obj_ref.num, 3);
  13. // You cannot get PyRefMut unless all PyRefs are dropped
  14. assert!(obj.try_borrow_mut().is_err());
  15. }
  16. {
  17. let mut obj_mut = obj.borrow_mut(); // Get PyRefMut
  18. obj_mut.num = 5;
  19. // You cannot get any other refs until the PyRefMut is dropped
  20. assert!(obj.try_borrow().is_err());
  21. assert!(obj.try_borrow_mut().is_err());
  22. }
  23. // You can convert `&PyCell` to a Python object
  24. pyo3::py_run!(py, obj, "assert obj.num == 5")

&PyCell<T>GILGuard 的生命周期相同。为了延长对象的生命周期(例如,将其存储在一个 Rust 的结构中),你可以使用 Py<T> ,它存储一个比GIL生存期更长的对象,因此需要 Python<'_> 令牌来访问。

  1. #[pyclass]
  2. struct MyClass {
  3. num: i32,
  4. }
  5. fn return_myclass() -> Py<MyClass> {
  6. let gil = Python::acquire_gil();
  7. let py = gil.python();
  8. Py::new(py, MyClass { num: 1 }).unwrap()
  9. }
  10. let gil = Python::acquire_gil();
  11. let obj = return_myclass();
  12. let cell = obj.as_ref(gil.python()); // Py<MyClass>::as_ref returns &PyCell<MyClass>
  13. let obj_ref = cell.borrow(); // Get PyRef<T>
  14. 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 {

  1. #[new]
  2. fn new(num: i32) -> Self {
  3. MyClass { num }
  4. }

}

  1. <a name="EpdHa"></a>
  2. ### 返回类型
  3. <a name="zwNNR"></a>
  4. ### 继承
  5. <a name="iCXah"></a>
  6. ### 对象属性
  7. <a name="bRRWS"></a>
  8. ### 实例方法
  9. <a name="18wAL"></a>
  10. ### 类方法
  11. <a name="v9U1A"></a>
  12. ### 静态方法
  13. <a name="IWuaa"></a>
  14. ## 类型转换
  15. 在本部分中,我们将讨论Python类型到PyO3提供的Rust类型的映射,以及在它们之间执行转换所需的特征。
  16. <a name="XBtty"></a>
  17. ### 将Rust类型映射到Python类型
  18. 在编写可从Python调用的函数时(例如`#[pyfunction]`或`#[pymethods]`块),函数参数需要特征`FromPyObject`,函数返回值需要`IntoPy<PyObject>`。<br />请查阅下一节中的表,以查找实现这些特征的PyO3提供的Rust类型。
  19. <a name="EJOJc"></a>
  20. #### 参数类型
  21. 接受函数参数时,可以使用 Rust 库类型或 PyO3 的Python原生类型。 (有关如何使用它们的讨论,请参见下一节。)<br />下表包含Python类型和将接受它们的相应函数参数类型:
  22. | **Python** | **Rust** | **Rust (Python-native)** |
  23. | --- | --- | --- |
  24. | `object` | - | `&PyAny` |
  25. | `str` | `String`, `Cow<str>`, `&str` | `&PyUnicode` |
  26. | `bytes` | `Vec<u8>`, `&[u8]` | `&PyBytes` |
  27. | `bool` | `bool` | `&PyBool` |
  28. | `int` | Any integer type (`i32`, `u32`, `usize`, etc) | `&PyLong` |
  29. | `float` | `f32`, `f64` | `&PyFloat` |
  30. | `complex` | `num_complex::Complex`[1](https://pyo3.rs/v0.13.2/conversions/tables.html#1) | `&PyComplex` |
  31. | `list[T]` | `Vec<T>` | `&PyList` |
  32. | `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` |
  33. | `tuple[T, U]` | `(T, U)`, `Vec<T>` | `&PyTuple` |
  34. | `set[T]` | `HashSet<T>`, `BTreeSet<T>`, `hashbrown::HashSet<T>`[2](https://pyo3.rs/v0.13.2/conversions/tables.html#2) | `&PySet` |
  35. | `frozenset[T]` | `HashSet<T>`, `BTreeSet<T>`, `hashbrown::HashSet<T>`[2](https://pyo3.rs/v0.13.2/conversions/tables.html#2) | `&PyFrozenSet` |
  36. | `bytearray` | `Vec<u8>` | `&PyByteArray` |
  37. | `slice` | - | `&PySlice` |
  38. | `type` | - | `&PyType` |
  39. | `module` | - | `&PyModule` |
  40. | `datetime.datetime` | - | `&PyDateTime` |
  41. | `datetime.date` | - | `&PyDate` |
  42. | `datetime.time` | - | `&PyTime` |
  43. | `datetime.tzinfo` | - | `&PyTzInfo` |
  44. | `datetime.timedelta` | - | `&PyDelta` |
  45. | `typing.Optional[T]` | `Option<T>` | - |
  46. | `typing.Sequence[T]` | `Vec<T>` | `&PySequence` |
  47. | `typing.Iterator[Any]` | - | `&PyIterator` |
  48. | `typing.Union[...]` | See [`#[derive(FromPyObject)]`](https://pyo3.rs/v0.13.2/conversions/traits.html#deriving-a-hrefhttpsdocsrspyo3latestpyo3conversiontraitfrompyobjecthtmlfrompyobjecta-for-enums) | - |
  49. 还有一些与 GIL 和Rust定义的`#[pyclass]` 等相关的特殊类型,可能会有用:
  50. | **What** | **Description** |
  51. | --- | --- |
  52. | `Python` | A GIL token, used to pass to PyO3 constructors to prove ownership of the GIL |
  53. | `Py<T>` | A Python object isolated from the GIL lifetime. This can be sent to other threads. |
  54. | `PyObject` | An alias for `Py<PyAny>` |
  55. | `&PyCell<T>` | A `#[pyclass]` value owned by Python. |
  56. | `PyRef<T>` | A `#[pyclass]` borrowed immutably. |
  57. | `PyRefMut<T>` | A `#[pyclass]` borrowed mutably. |
  58. 要了解接受`#[pyclass]`值作为函数参数的更多细节,请参阅本Python类指南的章节。
  59. <a name="b2VZq"></a>
  60. #### 将Rust值返回给Python
  61. 在可从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作为返回值:
  62. | **Rust type** | **Resulting Python Type** |
  63. | --- | --- |
  64. | `String` | `str` |
  65. | `&str` | `str` |
  66. | `bool` | `bool` |
  67. | Any integer type (`i32`, `u32`, `usize`, etc) | `int` |
  68. | `f32`, `f64` | `float` |
  69. | `Option<T>` | `Optional[T]` |
  70. | `(T, U)` | `Tuple[T, U]` |
  71. | `Vec<T>` | `List[T]` |
  72. | `HashMap<K, V>` | `Dict[K, V]` |
  73. | `BTreeMap<K, V>` | `Dict[K, V]` |
  74. | `HashSet<T>` | `Set[T]` |
  75. | `BTreeSet<T>` | `Set[T]` |
  76. | `&PyCell<T: PyClass>` | `T` |
  77. | `PyRef<T: PyClass>` | `T` |
  78. | `PyRefMut<T: PyClass>` | `T` |
  79. <a name="NB75x"></a>
  80. ## 转化 Trait
  81. PyO3提供了一些方便的特性,可以在Python类型和Rust类型之间进行转换。
  82. <a name="apruA"></a>
  83. ### `.extract()` 和 `FromPyObject` trait
  84. 将Python对象转换为Rust值的最简单方法是使用`.extract()`。 如果转换失败,它将返回带有类型错误的 `PyResult`,因此通常您会使用类似:
  85. ```rust
  86. let v: Vec<i32> = obj.extract()?;

此方法可用于许多Python对象类型,并且可以生成多种Rust类型,您可以在 FromPyObject 的实现者列表中进行检查。
FromPyObject 也可以用于包装为Python对象的Rust类型(参见关于类的章节)。在这里,为了既能够在可变引用上操作,又满足Rust的非混淆可变引用规则,必须提取PyO3引用包装器PyRefPyRefMut。它们的工作方式类似于std::cell::RefCell的引用包装器,并确保(在运行时)允许Rust借用。

Python异常处理

定义一个新的异常

您可以使用create_exception! 宏来定义新的异常类型:

  1. use pyo3::create_exception;
  2. create_exception!(module, MyError, pyo3::exceptions::PyException);
  • module是包含模块的名称。
  • MyError是新异常类型的名称。

例如:

  1. use pyo3::prelude::*;
  2. use pyo3::create_exception;
  3. use pyo3::types::IntoPyDict;
  4. use pyo3::exceptions::PyException;
  5. create_exception!(mymodule, CustomError, PyException);
  6. fn main() {
  7. let gil = Python::acquire_gil();
  8. let py = gil.python();
  9. let ctx = [("CustomError", py.get_type::<CustomError>())].into_py_dict(py);
  10. py.run("assert str(CustomError) == \"<class 'mymodule.CustomError'>\"", None, Some(&ctx)).unwrap();
  11. py.run("assert CustomError('oops').args == ('oops',)", None, Some(&ctx)).unwrap();
  12. }

使用PyO3创建扩展模块时,您可以像这样向模块添加新的异常,以便可以从Python导入它:

  1. create_exception!(mymodule, CustomError, PyException);
  2. #[pymodule]
  3. fn mymodule(py: Python, m: &PyModule) -> PyResult<()> {
  4. // ... other elements added to module ...
  5. m.add("CustomError", py.get_type::<CustomError>())?;
  6. Ok(())
  7. }

抛出异常

要引发异常,首先需要获取异常类型并构造一个新的PyErr,然后调用PyErr::restore方法将异常写回到 Python 解释器的全局状态。

  1. use pyo3::{Python, PyErr};
  2. use pyo3::exceptions::PyTypeError;
  3. fn main() {
  4. let gil = Python::acquire_gil();
  5. let py = gil.python();
  6. PyTypeError::new_err("Error").restore(py);
  7. assert!(PyErr::occurred(py));
  8. drop(PyErr::fetch(py));
  9. }

检查异常类型

处理Rust错误

使用Python代码中定义的异常

  1. use pyo3::prelude::*;
  2. mod io {
  3. pyo3::import_exception!(io, UnsupportedOperation);
  4. }
  5. fn tell(file: &PyAny) -> PyResult<u64> {
  6. use pyo3::exceptions::*;
  7. match file.call_method0("tell") {
  8. Err(_) => Err(io::UnsupportedOperation::new_err("not supported: tell")),
  9. Ok(x) => x.extract::<u64>(),
  10. }
  11. }