尝试rust与python的混合编程(一)

前言

笔者想试试rust与python的混合编程,笔者去搜索了一下

PyO3 教程:连接 Python 与 Rust 的桥梁PyO3 是一个 Rust 库,它提供了 Rust 和 Pyth - 掘金https://juejin.cn/post/7486766400281231414(35 封私信 / 38 条消息) Python+Rust混合编程 - 知乎https://zhuanlan.zhihu.com/p/1929138818348487898Rust 与 Python 混合编程 | 鸭梨公共文档https://docs.alexsun.top/public/blog/2024-12/rust-maturin.html感觉很好玩,笔者决定尝试一下,官网参考

FAQ and troubleshooting - PyO3 user guidehttps://pyo3.rs/Mapping of Rust types to Python types - PyO3 user guidehttps://pyo3.rs/v0.27.1/conversions/tables.html

正文

初始化项目

笔者初始化项目,笔者python的包管理工具是Rye,至于什么是Rye以及怎么使用这样,笔者直接跳过,而Rye可以初始化rust-python混合编程的项目

rye init rye-rp --build-system maturin

rye sync

就可以初始化一个rust-python混合编程的项目,项目结构如下

可以安装一个关键的依赖maturin

rye add --dev maturin

笔者对cargo.toml文件的依赖做出修改

rust 复制代码
[package]
name = "rye-rp"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

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

截止到目前最新版是0.27.1。

那么需要修改一下lib.rs里面的内容,修改更新一下,即

rust 复制代码
use pyo3::prelude::*;

#[pyfunction]
fn hello() -> PyResult<String> {
    Ok("Hello from rye-rp!".into())
}

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

在python/rye-rp目录下,新建一个**main.py**文件,或者使用如下代码

python 复制代码
if __name__ == "__main__":
    print(hello())

两种都行,笔者直接在**init.py**写打印语句,直接打印了两次

笔者还懵逼了一下,后面明白了

当直接运行 init.py 时,Python 加载器会让同一个文件被执行 两次,但以不同模块名加载:

第一次:作为 main 模块(直接运行)
第二次:作为 rye_rp.init 模块(被导入时)

总之,结果如下

没问题。

尝试打包

笔者添加一个简单的函数

rust 复制代码
#[pyfunction]
fn add(x: i32, y: i32) -> PyResult<i32> {
    Ok(x + y)
}

需要添加到模块里面

rust 复制代码
#[pymodule]
fn rust_python(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(hello, m)?)?;
    m.add_function(wrap_pyfunction!(add, m)?)?;
    Ok(())
}

笔者顺序修改一下名字。在项目根目录,运行如下命令

maturin develop --release

有个警告

Warning: Couldn't find the symbol `PyInit__lowlevel` in the native library. Python will fail to import this module. If you're using pyo3, check that `#[pymodule]` uses `_lowlevel` as module name 📦 Built wheel for CPython 3.13 to F:\code\Python\rye-rp\target\wheels\rye_rp-0.1.0-cp313-cp313-win_amd64.whl

好像是笔者修改了模块的名字,说导入失败了**_lowlevel**这个模块

修改在pyproject.toml文件中的内容

[tool.maturin]
python-source = "python"
module-name = "rye_rp.rust_python"
features = ["pyo3/extension-module"]

修改了一下模块的名字,再次运行,结果如下

没问题,可以发现,在python/rye-rp目录下出现了一个新的文件pyd文件

原来的文件被笔者删除了。修改__init__.py文件,内容如下

rust 复制代码
from rust_python import hello,add
if __name__ == '__main__':
    print(hello())
    print(add(1,2))

虽然说Pycharm有报错

但是运行没问题。

如何让pycharm不再报错,很简单,在rye-rp目录下,新建一个rust_python.pyi文件

如下大佬的文章

Python 存根文件(.pyi)详解:类型提示的高级指南-CSDN博客https://blog.csdn.net/weixin_63779518/article/details/148560726

其中内容如下

rust 复制代码
def hello() -> str:
    """返回问候语"""
    ...

def add(a: int, b: int) -> int:
    """两数相加"""
    ...

这个就不会报错了。


=========就这样,明天再说===========


导出类

简单的导出

导出函数使用的是pyfunction,那导出类自然使用的是pyclass。

简单的一个类student,先新建一个student文件

其中内容如下

rust 复制代码
use pyo3::prelude::*;
#[pyclass]
pub struct Student{
    #[pyo3(get,set)]
    pub id:u32,
    #[pyo3(get,set)]
    pub name:String,
}

实现了get和set。打包并运行,导入并使用

rust 复制代码
from rust_python import hello, add,Student
if __name__ == '__main__':
    print(hello())
    print(add(1,2))
    s=Student(1,"sasd")
    print(s)

然后就报错了

Hello from rye-rp!
3
Traceback (most recent call last):
File "F:\code\Python\rye-rp\python\rye_rp\init.py", line 5, in <module>
s=Student(1,"sasd")
TypeError: cannot create 'builtins.Student' instances

什么叫没有不能创建"builtins.Student"实例,啊

笔者去pyo3的官网看了看

好像需要实现一个new方法,应该是,行

rust 复制代码
#[pymethods]
impl Student{
     #[new] 
    fn new(id: u32, name: String) -> Self {
        Self { id, name }
    }
}

再次打包运行。结果如下

没问题。

当然,还是没有代码提示,写一个类

python 复制代码
class Student:
    id: int
    name: str
    def __init__(self, id: int, name: str) -> None: ...

作为提示。

实现魔术方法

比如__str__,__len__等之类的,因此,笔者重新定义一个类Vector,搞点好玩的,当然,还是在一个新的rust文件里面写。

当然Vector可以类似于Python的列表,那么应该比较灵活,除了能存放数字,还应该能存放其他东西,考虑PyAny。

必然需要使用Rust里面的Vec,

PyAny in pyo3 - Rusthttps://jonhue.github.io/soco/doc/pyo3/struct.PyAny.html如果还需要实现切片,这里就非常值得考虑了。

那么经过多次的修改,最终rust的代码如下,

具体细节难以细说,主要是因为还是比较麻烦。


=====算了,明天再说=======


直接给出代码

rust 复制代码
use pyo3::exceptions::PyIndexError;
use pyo3::prelude::*;
use pyo3::types::{PyAny, PySlice};

#[pyclass]
pub struct Vector {
    data: Vec<Py<PyAny>>,
}

#[pymethods]
impl Vector {
    #[new]
    fn new(data: Vec<Py<PyAny>>) -> Self {
        Self { data }
    }

    fn __len__(&self) -> usize {
        self.data.len()
    }

    fn __getitem__<'py>(&self, py: Python<'py>, idx: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyAny>> {
        if let Ok(i) = idx.extract::<isize>() {
            let len = self.data.len() as isize;
            let adjusted_i = if i < 0 { len + i } else { i };

            if adjusted_i < 0 || adjusted_i >= len {
                return Err(PyIndexError::new_err("Index out of bounds"));
            }

            return self.data.get(adjusted_i as usize)
                .map(|obj| obj.bind(py).clone())
                .ok_or_else(|| PyIndexError::new_err("Index out of bounds"));
        }
    
        if let Ok(slice) = idx.cast::<PySlice>() {
            let indices = slice.indices(self.data.len() as isize)?;
            let mut items = Vec::with_capacity(indices.slicelength as usize);

            let mut current = indices.start;
            for _ in 0..indices.slicelength {
                if let Some(obj) = self.data.get(current as usize) {
                    items.push(obj.clone_ref(py));
                }
                current += indices.step;
            }

            let new_vector = Vector { data: items };
            let py_vector = Py::new(py, new_vector)?;
            Ok(py_vector.bind(py).clone().into_any()) 
        } else {
            Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
                "Index must be int or slice"
            ))
        }
    }
     fn __str__(&self, py: Python<'_>) -> PyResult<String> {
        let mut parts = Vec::with_capacity(self.data.len());
        for obj in &self.data {
            let s = obj.bind(py).str()?.to_string();
            parts.push(s);
        }
        Ok(format!("Vector([{}])", parts.join(", ")))
    }
    fn __repr__(&self, py: Python<'_>) -> PyResult<String> {
        let mut parts = Vec::with_capacity(self.data.len());
        for obj in &self.data {
            let s = obj.bind(py).repr()?.to_string();
            parts.push(s);
        }
        Ok(format!("Vector([{}])", parts.join(", ")))
    }
}
python 复制代码
from typing import Any, overload, Union
class Vector:
    def __init__(self, data: list[Any]) -> None:...

    def __len__(self) -> int:        ...

    def __str__(self) -> str:...

    def __repr__(self) -> str:...

    @overload
    def __getitem__(self, idx: int) -> Any:...

    @overload
    def __getitem__(self, idx: slice) -> "Vector":...

    def __getitem__(self, idx: Union[int, slice]) -> Union[Any, "Vector"]:...

打包、安装、运行

python 复制代码
    v=Vector([1,2,3,4,5])
    print(len(v))
    print(v[-2])

一些关键的东西

Bound和Py

Bound in pyo3 - Rusthttps://pyo3.rs/main/doc/pyo3/struct.bound

看看Bound的定义

rust 复制代码
#[repr(transparent)]
pub struct Bound<'py, T>(Python<'py>, ManuallyDrop<Py<T>>);

这是一个**元组结构体,**如果初始化需要传两个参数,可以写一个测试,来看看到底怎么使用的

使用官网的例子

rust 复制代码
use pyo3::prelude::*;
use std::any::type_name_of_val;
#[pyclass]
struct  Foo{
    #[pyo3(get, set)]
    name: String,
}
#[pyclass]
struct  Foo{
    name: String,
}
#[pymethods]
impl Foo{
    #[new]
    fn new(name: String) -> Self {
        Self{
            name,
        }
    }
}
#[cfg(test)]
mod tests{
    use super::*;
    use pyo3::Python;

    #[test]
    fn test_bound(){
        Python::attach(|py|{
            let foo_bound=Bound::new(py,Foo::new("foo".to_string()));
            println!("  类型: {}", type_name_of_val(&foo_bound))
        })


    }
}

笔者运行测试,没想到报错了

error: test failed, to rerun pass `-p rye-rp --lib`

Caused by:
process didn't exit successfully: `C:\Users\26644\AppData\Local\Programs\RustRover 2025.2.4.1\bin\native-helper\intellij-rust-native-helper.exe F:\code\Python\rye-rp\target\debug\deps\rye_rp-2c705f678bff614e.exe simple::tests::test_bound --format=json --exact -Z unstable-options --show-output` (exit code: 0xc0000135, STATUS_DLL_NOT_FOUND)
note: test exited abnormally; to see the full output pass --no-capture to the harness.
error: 1 target failed:
`-p rye-rp --lib`

这里面有个关键的东西------STATUS_DLL_NOT_FOUND,说明是dll文件没有发现,那么显示是指的python.dll文件了,看来测试环境和部署环境还不一样,获取不了dll文件

笔者进入虚拟环境里面的Scripts看看

笔者使用的rye,笔者使用的是python313,这里面确实是没有python311.dll文件,有点意思

因此,笔者直接把rye管理的python313.dll文件复制到这个Scripts目录下。然后再运行

thread 'simple::tests::test_bound' (51684) panicked at C:\Users\26644\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\pyo3-0.27.1\src\interpreter_lifecycle.rs:117:13:
assertion `left != right` failed: The Python interpreter is not initialized and the `auto-initialize` feature is not enabled.

Consider calling `Python::initialize()` before attempting to use Python APIs.
left: 0
right: 0
stack backtrace:

.....

需要先调用**Python::initialize(),**可以,修改一下代码

rust 复制代码
    #[test]
    fn test_bound(){
        Python::initialize();
        Python::attach(|py|{
            let foo_bound=Bound::new(py,Foo::new("foo".to_string())).unwrap();
            println!("  类型: {}", type_name_of_val(&foo_bound));
            let foo:Py<Foo>=foo_bound.into();
            println!("  类型: {}", type_name_of_val(&foo));
        })

    }

结果如下


=========就这样,明天再说============


简单的测试了一下,

这里使用了两个关键的东西Bound和Py,先给出Py的定义

rust 复制代码
#[repr(transparent)]
pub struct Py<T>(NonNull<ffi::PyObject>, PhantomData<T>);

Bound是有生命周期参数,而Py没有这个参数,这就非常关键了

直接复制官网的原文

bound

A Python thread-attached equivalent to Py<T>.

This type can be thought of as equivalent to the tuple (Py<T>, Python<'py>). By having the 'py lifetime of the Python<'py> token, this ties the lifetime of the Bound<'py, T> smart pointer to the lifetime the thread is attached to the Python interpreter and allows PyO3 to call Python APIs at maximum efficiency.

To access the object in situations where the thread is not attached, convert it to Py<T> using .unbind(). This includes, for example, usage in Python::detach's closure.

See the guide for more detail.

Py

A reference to an object allocated on the Python heap.

Py只是一个reference,有python对象的地址,可以知道里面的数据和类型,但是不能安全地操作

Bound是A Python thread-attached equivalent to Py<T>,说白了,二者根本性的区别

就是能不能安全地操作的问题,Py里面有数据和类型,bound里面也有数据和类型,

但是bound是Python thread-attached,而这个 thread-attached的意思

thread-attached = 当前线程持有 GIL=可以CRUD

就这个意思。

总之,二者的根本性区别就是能否操作Python对象,笔者个人的理解

写一个测试------test_alter_name

rust 复制代码
    #[test]
    fn test_bound(){
        Python::initialize();
        Python::attach(|py|{
            let foo_bound=Bound::new(py,Foo::new("foo".to_string())).unwrap();
            println!("  类型: {}", type_name_of_val(&foo_bound));
            foo_bound.borrow_mut().name = "foo".to_string();
            let foo:Py<Foo>=foo_bound.into();
            foo.borrow_mut(py).name = "foo".to_string();
            println!("  类型: {}", type_name_of_val(&foo));
        })
    }

初始化Bound,使用borrow_mut修改内容,变成Py之后,也使用borrow_mut修改内容

看一些Py中borrow_mut的函数签名

rust 复制代码
    #[inline]
    #[track_caller]
    pub fn borrow_mut<'py>(&'py self, py: Python<'py>) -> PyRefMut<'py, T>
    where
        T: PyClass<Frozen = False>,
    {
        self.bind(py).borrow_mut()
    }

可以发现需要传入一个参数py ,这个参数py的类型是Python,

其中函数体,先使用bind,然后调用borrow_mut

那么显然,这个bind是把Py变成了Bound,然后调用Bound的borrow_mut。

可以看一下bind方法

rust 复制代码
    #[inline]
    pub fn bind<'py>(&self, _py: Python<'py>) -> &Bound<'py, T> {
        // SAFETY: `Bound` has the same layout as `Py`
        unsafe { NonNull::from(self).cast().as_ref() }
    }

没问题。

Python是什么

前面看了Py和Bound,发现Py变成Bound需要一个参数,这个参数的类型是Python,这个Python又是什么?

rust 复制代码
#[derive(Copy, Clone)]
pub struct Python<'py>(PhantomData<&'py AttachGuard>, PhantomData<NotSend>);

首先这个是一个元组结构体。
3.10 PhantomData(幽灵数据) | 第三章、所有权 |《Rust 高级编程 2018》| Rust 技术论坛https://learnku.com/docs/nomicon/2018/310-phantom-data/4721PhantomData<T> 是 Rust 标准库中的零大小类型,用于在编译期向 Rust 的类型系统提供类型信息,运行时完全不存在。

总之,注意力放到**&'py AttachGuard,这是什么**

rust 复制代码
/// RAII type that represents thread attachment to the interpreter.
pub(crate) enum AttachGuard {
    /// Indicates the thread was already attached when this AttachGuard was acquired.
    Assumed,
    /// Indicates that we attached when this AttachGuard was acquired
    Ensured { gstate: ffi::PyGILState_STATE },
}

RAII type that represents thread attachment to the interpreter.

翻译一下------一个 RAII 类型,表示线程已附加到解释器

说白了这个Python这个类型就是一个证明、一个Token,

就相当于前端用户登录,后端返回登录信息,一个Token,放到Cookie里面,而这个Python就是这个作用

总之,就是有了这个Python就代表已经是thread-attach

这也说明Bound<'py>=(Py,Python<'py>),或者说Bound<'py, T> ≈ Py<T> + Python<'py>

没问题

相关推荐
i***66503 小时前
SpringBoot实战(三十二)集成 ofdrw,实现 PDF 和 OFD 的转换、SM2 签署OFD
spring boot·后端·pdf
~无忧花开~3 小时前
JavaScript实现PDF本地预览技巧
开发语言·前端·javascript
vvoennvv3 小时前
【Python TensorFlow】 TCN-LSTM时间序列卷积长短期记忆神经网络时序预测算法(附代码)
python·神经网络·机器学习·tensorflow·lstm·tcn
靠沿3 小时前
Java数据结构初阶——LinkedList
java·开发语言·数据结构
4***99743 小时前
Kotlin序列处理
android·开发语言·kotlin
froginwe113 小时前
Scala 提取器(Extractor)
开发语言
t***D2643 小时前
Kotlin在服务端开发中的生态建设
android·开发语言·kotlin
qq_12498707533 小时前
基于springboot的建筑业数据管理系统的设计与实现(源码+论文+部署+安装)
java·spring boot·后端·毕业设计
nix.gnehc3 小时前
PyTorch
人工智能·pytorch·python