用 Rust 写 Polars 插件

脚本小子阿强 初级 3小时前 347 浏览 11 点赞 约 1 分钟

在处理百万级数据量时,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"

实战踩坑经验:
这里有个极其恶心的版本兼容问题。pyo3pyo3-polars 的版本必须严格匹配。如果你单独升级其中一个,可能会遇到极其晦涩的链接错误(Link Error),看起来像编译器 Bug,其实就是版本不兼容。建议把这两个依赖当成一个整体来管理,不要随意单独 bump 版本。

这种方案的本质是将计算下沉到 Rust 侧,Python 只负责发送指令,从而在保证开发效率的同时,获得接近原生的执行性能。

AI编程AI编程实战pythonrustpolars

全部回复 (3)

极客Ray 高级 9小时前
之前试过用这个写复杂逻辑,速度确实快了不止一倍。
0 回复
阿海爱学习 高级 9小时前
其实编译成 .so 文件后,多进程环境下内存占用也稳多了。
0 回复
阿小美 中级 9小时前
之前脱敏千万级日志就卡在 map 上,换成插件后跑起来顺畅多了。
0 回复

发表回复

支持 Markdown 格式