选自medium.com
作者: Carl M. Kadie
机器之心编译
编辑:杜伟、陈萍
效果好不好,试一试就知道了。
Python 是数据科学家最流行的编程语言之一,其内部集成了高质量分析库,包括 NumPy、SciPy、自然语言工具包等,这些库中的许多都是用 C 和 C++ 实现的。
然而,C 和 C++ 兼容性差,且本身不提供线程安全。有研究者开始转向 Rust,重写 C++ 扩展。
拥有 CS 与机器学习博士学位的 Carl M. Kadie ,通过更新 Python 中生物信息学软件包 Bed-Reader,为研究者带来了在 Rust 中编写 Python 扩展的九个规则。以下是原博客的主要内容。
一年前,我厌倦了我们软件包 Bed-Reader 的 C++ 扩展,我用 Rust 重写了它,令人高兴的是,得到的新扩展和 C/C++ 一样快,但具有更好的兼容性和安全性。一路走来,我学会了这九条规则,可以帮助你创建更好的扩展代码,这 九条规则 包括:
1. 创建一个包含 Rust 和 Python 项目的单独存储库
2. 使用 maturin & PyO3 在 Rust 中创建 Python-callable translator 函数
3. 让 Rust translator 函数调用 nice Rust 函数
4. 在 Python 中预分配内存
5. 将 nice Rust 错误处理翻译 nice Python 错误处理
6. 多线程与 Rayon 和 ndarray::parallel,返回任何错误
7. 允许用户控制并行线程数
8. 将 nice 动态类型 Python 函数翻译成 nice Rust 泛型函数
9. 创建 Rust 和 Python 测试
其中, 文中提到的 nice 这个词是指使用最佳实践和原生类型创建 。换句话说:在代码顶部,编写 nice Python 代码;在中间,用 Rust 编写 translator 代码;在底部,编写 nice Rust 代码。结构如下图所示:
上述策略看似显而易见,但遵循它可能会很棘手。本文提供了有关如何遵循每条规则的实用建议和示例。
我在 Bed-Reader 进行了实验, Bed-Reader 是一个 Python 包 ,用于读取和写入 PLINK Bed Files,这是一种在生物信息学中用于存储 DNA 数据的二进制格式。Bed 格式的文件可以达到 TB。Bed-Reader 让用户可以快速、随机地访问数据的子集。它在用户选择的 int8、float32 或 float64 中返回一个 NumPy 数组。
我希望 Bed-Reader 扩展代码具有以下特点:
-
比 Python 快;
-
兼容 NumPy;
-
可以进行数据并行多线程处理;
-
与执行数据并行多线程的所有其他包兼容;
-
安全。
我们最初的 C++ 扩展兼具速度快、与 NumPy 兼容,以及使用 OpenMP 进行数据并行多线程等特点。遗憾的是,OpenMP 运行时库 (Runtime library),存在 Python 包兼容版本问题。
Rust 提供了 C++ 扩展带来的优势。除此之外,Rust 通过提供没有运行时库的数据并行多线程解决了运行时兼容性问题。此外,Rust 编译器还能保证线程安全。
在 Rust 中创建 Python 扩展需要许多设计决策。根据我使用 Bed-Reader 的经验,以下是我的使用规则。
规则 1:创建一个包含 Rust 和 Python 项目的单独存储库
下表显示了如何布局文件:
使用 Rust 常用的‘cargo new’命令创建 Cargo.toml 和 src/lib.rs 文件。Python 没有 setup.py 文件。相反,Cargo.toml 包含 PyPi 包信息,例如包的名称、版本号、 README 文件的位置等。要在没有 setup.py 的情况下工作,pyproject.toml 必须包含:
[build-system]
requires = ["maturin==0.12.5"]
build-backend = "maturin"
一般来说,Python 设置在 pyproject.toml 中(如果不是,则在 pytest.ini 等文件中)。Python 代码位于子文件夹 bed_reader 中。
最后,我们使用 GitHub 操作来构建、测试和准备部署。该脚本位于 .github/workflows/ci.yml 中。
规则 2:使用 maturin & PyO3 在 Rust 中
创建 Python-callable translator 函数
Maturin 是一个 PyPi 包,可通过 PyO3 构建和发布 Python 扩展。PyO3 是一个 Rust crate,用于在 Rust 中编写 Python 扩展。
在 Cargo.toml 中,包含这些 Rust 依赖项:
[dependencies]
thiserror = "1.0.30"
ndarray-npy = { version = "0.8.1", default-features = false }
rayon = "1.5.1"
numpy = "0.