用 Rust 写 Polars 插件
在处理百万级数据量时,
下一篇
分享一个把多智能体工作流从冗余代码中解耦的方案 →
map_elements 简直是性能杀手。它每处理一行就要在 Rust 引擎和 Python 解释器之间跳一次,这种频繁的上下文切换会让 Polars 强大的向量化能力完全失效。如果你在做 PII(个人隐私信息)脱敏,比如用正则屏蔽掉支付备注里的银行卡号,用纯 Python 写会导致 CPU 大量时间浪费在对象分配和函数调用上。最硬核的解决方案是直接写一个 Polars 表达式插件。简单来说,就是用 Rust 实现逻辑,编译成动态链接库,然后让 Polars 在原生层面直接调用。
核心实现链路
一个插件由 Rust 端的逻辑实现和 Python 端的注册函数两部分组成。
1. Rust 端:实现处理函数
通过 pyo3-polars 提供的宏,可以直接定义一个操作 Series 的函数。
use pyo3_polars::derive::polars_expr;
use pyo3_polars::export::polars_core::prelude::*;
#[polars_expr(output_type=String)]
fn mask_digits(inputs: &[Series]) -> PolarsResult<Series> {
let ca = inputs[0].str()?;
let out: StringChunked = ca.apply(|opt| {
opt.map(|s| std::borrow::Cow::Owned(mask_cell(s)))
});
Ok(out.into_series())
}2. Python 端:注册并调用
在 Python 侧,你需要用 register_plugin_function 把编译好的库关联起来。
from pathlib import Path
import polars as pl
from polars.plugins import register_plugin_function
PLUGIN = Path(__file__).parent
def mask_digits(expr):
col = pl.col(expr) if isinstance(expr, str) else expr
return register_plugin_function(
plugin_path=PLUGIN,
function_name="mask_digits",
args=[col],
is_elementwise=True,
)
# 调用时和普通表达式一模一样
df.with_columns(mask_digits("notes"))部署与配置坑点
要让这套工作流跑通,Cargo.toml 的配置至关重要。必须将 crate-type 设置为 cdylib,这样才能生成 Python 可加载的 .so 或 .pyd 文件。
[lib]
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.25", features = ["extension-module"] }
pyo3-polars = { version = "0.23", features = ["derive"] }
polars-core = "0.50"
regex = "1"
once_cell = "1"实战踩坑经验:
这里有个极其恶心的版本兼容问题。pyo3 和 pyo3-polars 的版本必须严格匹配。如果你单独升级其中一个,可能会遇到极其晦涩的链接错误(Link Error),看起来像编译器 Bug,其实就是版本不兼容。建议把这两个依赖当成一个整体来管理,不要随意单独 bump 版本。
这种方案的本质是将计算下沉到 Rust 侧,Python 只负责发送指令,从而在保证开发效率的同时,获得接近原生的执行性能。