目 录CONTENT

文章目录

将 Rust 与 Python 集成以用于数据科学

Administrator
2026-01-24 / 0 评论 / 0 点赞 / 1 阅读 / 0 字

📢 转载信息

原文链接:https://www.kdnuggets.com/integrating-rust-and-python-for-data-science

原文作者:Shittu Olumide, Technical Content Specialist



Integrating Rust and Python for Data Science
Image by Author

 

Python在数据科学中的主导地位与局限性

 
Python之所以成为数据科学的首选语言,并非偶然。它拥有成熟的生态系统、较低的入门门槛,以及能够帮助用户快速实现想法的强大库。NumPypandasscikit-learnPyTorch 以及 Jupyter Notebook 共同构成了一套难以超越的工作流程,适用于数据探索、建模和沟通。对于大多数数据科学家而言,Python 不仅仅是一个工具,更是思维发生的“环境”。

然而,Python也有其自身的限制。随着数据集的增长、数据管道的日益复杂以及性能预期的提高,团队开始感受到性能瓶颈。一些操作的速度不如预期,内存使用也变得难以预测。在某些阶段,问题从“Python能否完成这项工作?”转变为“是否应该让Python完成所有工作?”

这时 Rust 就发挥作用了。它不是要取代 Python,也不是要求数据科学家重写所有代码,而是作为一种支撑层存在。Rust越来越多地应用于 Python 工具的底层,处理那些对性能、内存安全性和并发性要求极高的计算任务。许多人可能已经在不知不觉中受益于 Rust,例如通过 Polars 这样的库,或者隐藏在 Python 应用程序编程接口 (API) 后的 Rust 支持组件。

本文探讨的就是这种中间地带。我们并非主张 Rust 优于 Python 进行数据科学工作,而是展示两者如何协作,在保持 Python 生产力的同时,解决其固有的弱点。我们将研究 Python 的不足之处、Rust 如何融入现代数据栈,以及实际的集成方式。

 

识别 Python 在数据科学工作负载中的瓶颈

 
Python最大的优势也是其最大的限制。该语言的优化目标是开发者生产力,而非原始执行速度。对于许多数据科学任务来说,这没有问题,因为繁重的工作是由优化的原生库完成的。当你用 pandas 编写 df.mean() 或用 NumPy 编写 np.dot() 时,你实际上并没有在 Python 中进行循环;你是在调用编译后的代码。

当你的工作负载不能干净地与这些基本操作对齐时,问题就会出现。一旦你开始在 Python 中进行循环操作,性能就会迅速下降。即使是编写良好的代码,当应用于数千万甚至上亿条记录时,也可能成为瓶颈。

内存是另一个压力点。Python 对象会带来显著的开销,而数据管道通常涉及重复的序列化和反序列化步骤。同样,在 pandas、NumPy 和外部系统之间移动数据时,可能会创建难以察觉、更难控制的数据副本。在大型管道中,内存使用通常成为作业变慢或失败的首要原因,而不是中央处理器 (CPU) 的使用率。

并发性则更具挑战性。Python 的全局解释器锁 (GIL) 虽然简化了许多事情,但它限制了 CPU 密集型工作真正的并行执行。虽然有方法可以规避这一点,例如使用多进程、原生扩展或分布式系统,但每种方法都有其自身的复杂性。

 

使用 Python 进行编排,使用 Rust 进行执行

 
将 Rust 和 Python 协同工作的最实用方法是划分职责。Python 负责编排,处理数据加载、定义工作流、表达意图和连接系统等任务。Rust 则接管执行细节至关重要的地方,例如紧密的循环、繁重的转换、内存管理和并行工作。

遵循此模型,Python 仍然是你大部分时间编写和阅读的语言。它是你构建分析、原型化想法和粘合组件的地方。Rust 代码则位于清晰的边界之后,负责实现那些昂贵、频繁执行或难以用 Python 高效表达的特定操作。这个边界是明确且经过深思熟虑的。

最具挑战性的任务之一是决定代码属于哪一部分;这最终归结为几个关键问题。如果代码经常更改、严重依赖实验,或受益于 Python 的表达能力,那么它可能属于 Python。但是,如果代码稳定且对性能至关重要,Rust 则更合适。数据解析、自定义聚合、特征工程内核和验证逻辑是常见的、非常适合 Rust 的用例。

这种模式在现代数据工具中已经存在,即使最终用户没有意识到这一点。Polars 使用 Rust 作为其执行引擎,同时暴露 Python API。Apache Arrow 的部分内容是用 Rust 实现并通过 Python 消费的。甚至 pandas 也越来越多地依赖于 Arrow 支持的原生组件来处理性能敏感的路径。这个生态系统正在悄然趋同于同一个理念:Python 作为接口,Rust 作为引擎。

这种方法的主要好处是它保留了生产力。你不会失去 Python 的生态系统或可读性。你可以在真正需要的地方获得性能提升,而无需将数据科学代码库变成一个系统编程项目。如果处理得当,大多数用户只会与干净的 Python API 交互,根本不需要关心底层是否涉及 Rust。

 

了解 Rust 和 Python 实际如何集成

 
在实践中,只要避免不必要的抽象,Rust 和 Python 的集成比听起来要简单。目前最常见的方法是使用 PyO3。PyO3 是一个 Rust 库,它允许使用 Rust 编写原生的 Python 扩展。你编写 Rust 函数和结构体,对它们进行注释,并将它们暴露为 Python 可调用的对象。从 Python 角度来看,它们表现得就像常规模块一样,具有标准的导入和文档字符串。

一个典型的设置如下:Rust 代码实现一个操作数组或 Arrow 缓冲区的函数,处理繁重的计算,并以 Python 友好的格式返回结果。PyO3 负责引用计数、错误转换和类型转换。像 maturinsetuptools-rust 这样的工具会将该扩展打包,以便可以使用 pip 进行安装,就像安装任何其他依赖项一样。

分发在整个故事中起着关键作用。过去,构建支持 Rust 的 Python 包很困难,但工具链已大大改善。如今,主流平台的主要预编译轮子文件 (wheels) 已经很常见,持续集成 (CI) 流水线可以自动生成它们。对于大多数用户来说,安装过程与安装纯 Python 库没有区别。

跨越 Python 和 Rust 边界会带来成本,无论是在运行时开销还是维护方面。这就是技术债务可能悄然积累的地方——如果 Rust 代码开始泄露特定于 Python 的假设,或者接口变得过于细粒度,复杂性就会超过收益。这就是为什么大多数成功的项目都维护一个稳定的边界。

 

使用 Rust 加速数据操作

 
为了说明这一点,我们考虑一个大多数数据科学家经常遇到的场景。你有一个大型的内存数据集,包含数千万行,并且需要应用一个自定义转换,而这个转换无法用 NumPy 或 pandas 进行向量化。它不是内置的聚合,而是特定于领域的逻辑,逐行运行并成为管道中的主要成本所在。

想象一个简单的情况:跨大型数组计算带有条件逻辑的滚动分数。在 pandas 中,这通常会导致 loopapply,一旦数据不再能整齐地适应向量化操作,两者都会变得非常慢。

 

示例 1:Python 基线

def score_series(values): out = [] prev = 0.0 for v in values: if v > prev: prev = prev * 0.9 + v else: prev = prev * 0.5 out.append(prev) return out

 

这段代码可读性强,但它是 CPU 密集型且单线程的。在大型数组上,它会变得异常缓慢。Rust 中相同的逻辑也很直接,更重要的是,它速度很快。Rust 紧凑的循环、可预测的内存访问和轻松的并行化在这里能带来巨大差异。

 

示例 2:使用 PyO3 实现

use pyo3::prelude::*; #[pyfunction] fn score_series(values: Vec<f64>) -> Vec<f64> { let mut out = Vec::with_capacity(values.len()); let mut prev = 0.0; for v in values { if v > prev { prev = prev * 0.9 + v; } else { prev = prev * 0.5; } out.push(prev); } out } #[pymodule] fn fast_scores(_py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(score_series, m)?)?; Ok(()) }

 

通过 PyO3 暴露后,这个函数可以像任何其他模块一样从 Python 中导入和调用。

from fast_scores import score_series result = score_series(values)

 

在基准测试中,性能提升通常是显著的。在 Python 中需要数秒或数分钟的操作,在 Rust 中可能缩短到毫秒或秒级。原始执行时间得到了显著改善。CPU 利用率增加,代码在更大输入上性能更好。内存使用也变得更可预测,在负载下惊喜更少。

系统整体复杂性没有增加;你现在需要管理两种语言和一个打包管道。当出现问题时,问题可能存在于 Rust 而非 Python 中。

 

示例 3:自定义聚合逻辑

你有一个大型数值数据集,需要一个在 pandas 或 NumPy 中无法干净向量化的自定义聚合。这种情况通常出现在特定领域的评分、规则引擎或特征工程逻辑中。

这是 Python 版本:

def score(values): total = 0.0 for v in values: if v > 0: total += v ** 1.5 return total

 

这具有可读性,但它是 CPU 密集型且单线程的。我们来看看 Rust 实现。我们将循环移到 Rust 中,并使用 PyO3 将其暴露给 Python。

Cargo.toml 文件

[lib] name = "fastscore" crate-type = ["cdylib"] [dependencies] pyo3 = { version = "0.21", features = ["extension-module"] }

 

src/lib.rs

use pyo3::prelude::*; #[pyfunction] fn score(values: Vec<f64>) -> f64 { values .iter() .filter(|v| **v > 0.0) .map(|v| v.powf(1.5)) .sum() } #[pymodule] fn fastscore(_py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(score, m)?)?; Ok(()) }

 

现在我们在 Python 中使用它:

import fastscore data = [1.2, -0.5, 3.1, 4.0] result = fastscore.score(data)

 

但为什么这可行呢?Python 仍然控制着工作流。Rust 只处理紧凑的循环。跨语言没有业务逻辑的分裂;相反,执行发生在最需要的地方。

 

示例 4:使用 Apache Arrow 共享内存

你希望在没有序列化开销的情况下在 Python 和 Rust 之间移动大型表格数据。在 DataFrame 之间来回转换会严重影响性能和内存。解决方案是使用 Arrow,它提供了一种 Python 和 Rust 都可以原生理解的共享内存格式。这通常是性能提升最大的地方——不是重写算法,而是避免不必要的数据移动。

以下是创建 Arrow 数据的 Python 代码:

import pyarrow as pa import pandas as pd df = pd.DataFrame({ "a": [1, 2, 3, 4], "b": [10.0, 20.0, 30.0, 40.0], }) table = pa.Table.from_pandas(df)

 

此时,数据已存储在 Arrow 的列式格式中。让我们编写 Rust 代码来消费 Arrow 数据,在 Rust 中使用 Arrow crate:

use arrow::array::{Float64Array, Int64Array}; use arrow::record_batch::RecordBatch; fn process(batch: &RecordBatch) -> f64 { let a = batch .column(0) .as_any() .downcast_ref::<Int64Array>() .unwrap(); let b = batch .column(1) .as_any() .downcast_ref::<Float64Array>() .unwrap(); let mut sum = 0.0; for i in 0..batch.num_rows() { sum += a.value(i) as f64 * b.value(i); } sum }

 

 

数据科学家关注的 Rust 工具

 
Rust 在数据科学中的作用不仅限于自定义扩展。越来越多的核心工具是用 Rust 编写的,并且在静默地支持 Python 工作流。Polars 是最明显的例子。它提供了一个类似于 pandas 的 DataFrame API,但它是基于 Rust 执行引擎构建的。

Apache Arrow 扮演着不同但同样重要的角色。它定义了一种 Python 和 Rust 都可以原生理解的列式内存格式。Arrow 使得在系统之间传输大型数据集无需复制或序列化成为可能。这通常是性能提升最大的领域——不是通过重写算法,而是通过避免不必要的数据移动。

 

确定何时不应选择 Rust

 
到目前为止,我们已经展示了 Rust 的强大功能,但它并非适用于每个数据问题的默认升级方案。在许多情况下,Python 仍然是合适的工具。

如果你的工作负载主要是 I/O 密集型的,涉及编排 API、运行结构化查询语言 (SQL) 或粘合现有库,那么 Rust 不会带来太多好处。在常见的科学数据工作流程中,大部分繁重工作已经由优化的 C、C++ 或 Rust 扩展来完成。在此基础上再用 Rust 包装更多代码,往往会增加复杂性而没有实际收益。

团队技能的重要性胜过基准测试。引入 Rust 意味着引入一门新语言、一个新的构建工具链和一个更严格的编程模型。如果只有一个人理解 Rust 层,那么这段代码就成了维护风险。调试跨语言问题也可能比修复纯 Python 问题要慢。

还存在过早优化的风险。很容易发现一个缓慢的 Python 循环并认为 Rust 是答案。通常,真正的修复方法是向量化、更好地利用现有库或使用不同的算法。过早转向 Rust 可能会让你陷入更复杂的设计中,而你还没有完全理解问题所在。

一个简单的决策清单可以提供帮助:

  • 代码是 CPU 密集型的并且已经结构良好吗?
  • 性能分析是否显示了 Python 难以合理优化的明显热点?
  • Rust 组件的重用率是否足以证明其引入的成本是合理的?

如果这些问题的答案不是明确的“是”,那么坚持使用 Python 通常是更好的选择。

 

结论

 
Python 在数据科学领域仍然处于前沿地位;迄今为止它仍然非常流行且有用。你可以执行从探索到模型集成等的各种活动。而 Rust 则加强了其基础。当性能、内存控制和可预测性变得重要时,Rust 就变得必不可少。有选择地使用它,可以让你超越 Python 的限制,而不会牺牲使数据科学家能够高效工作和快速迭代的生态系统。

最有效的方法是从小处着手,识别一个瓶颈,然后用一个支持 Rust 的组件替换它。在此之后,你需要衡量结果。如果有效,则小心地扩展;如果无效,则简单地回滚。
 
 

Shittu Olumide 是一位软件工程师和技术作家,热衷于利用尖端技术来构建引人入胜的叙事,他对细节有敏锐的洞察力,并且擅长简化复杂的概念。你也可以在 Twitter 上找到 Shittu。




🚀 想要体验更好更全面的AI调用?

欢迎使用青云聚合API,约为官网价格的十分之一,支持300+全球最新模型,以及全球各种生图生视频模型,无需翻墙高速稳定,文档丰富,小白也可以简单操作。

0

评论区