
扩展 Polars Python API用 register_*_namespace 为 Expr / DataFrame / LazyFrame / Series 注册自定义命名空间【免费下载链接】polarsExtremely fast Query Engine for DataFrames, written in Rust项目地址: https://gitcode.com/GitHub_Trending/po/polars本文以 Polars 官方 Python API 参考中的 Extending the API 页面 为骨架系统讲解如何在不使用子类化subclassing或 mixin 的前提下把自定义领域能力挂载到 Polars 四大核心对象的专属命名空间上如expr.custom.method()、df.split.by_alternate_rows()。读完本文你将掌握polars.api提供的四个注册入口、命名冲突规则、四类对象的完整实战示例以及这套机制在 api.py 中的描述符descriptor级实现原理。这个特性解决什么问题Polars 的Expr、DataFrame、LazyFrame、Series四类对象各自拥有一批内置命名空间如字符串操作.str、时间操作.dt、列表操作.list。当你想为数据清洗、金融计算、领域建模等场景提供一批开箱即用的高层方法时如果采用子类化或 mixin会让代码与 Polars 生态脱节也难以在.select、.with_columns等链式调用中被自然使用。官方为此提供了注册式扩展机制通过装饰器把一个自定义 Python 类注册为某一对象类型的命名空间之后即可用属性访问形式调用其中方法。原文档明确指出该特性主要为库作者设计用于提供核心库中不存在或不应存在的领域专属能力方法内部仍复用官方公开 API 组合实现保持结果类型与 Polars 一致、可继续参与表达式组合与惰性查询规划。注册之后无需任何额外调用步骤自定义类即成为该进程内所有Expr/DataFrame/LazyFrame/Series实例上的可用属性这比函数式封装更符合 Polars 的链式风格。四个注册入口一览自定义命名空间的功能统一收口在polars.api模块中实现在 py-polars/src/polars/api.py共四个公开装饰器即该模块__all__的四个元素装饰器作用于对象构造函数接收参数示例调用方式register_expr_namespace(name)Exprexpr: pl.Exprpl.all().greetings.hello()register_dataframe_namespace(name)DataFramedf: pl.DataFramedf.split.by_alternate_rows()register_lazyframe_namespace(name)LazyFrameldf: pl.LazyFrameldf.types.upcast_integer_types()register_series_namespace(name)Seriess: pl.Seriess.math.square()四个函数在源码层面最终都收敛到同一个工厂函数_create_namespace(name, cls)见 api.py#L48-L71只是分别绑定了pl.Expr/pl.DataFrame/pl.LazyFrame/pl.Series作为目标类register_expr_namespace→_create_namespace(name, pl.Expr)定义于 api.py#L74register_dataframe_namespace→_create_namespace(name, pl.DataFrame)定义于 api.py#L127register_lazyframe_namespace→_create_namespace(name, pl.LazyFrame)定义于 api.py#L225register_series_namespace→_create_namespace(name, pl.Series)定义于 api.py#L326。装饰器名中的name即对外暴露的命名空间名例如注册greetings后即可写作expr.greetings.hello()。该名会通过setattr(cls, name, NameSpace(...))动态设置到目标类上并同步加入该类的_accessors集合。命名冲突规则哪些名字不能被占用这是使用注册 API 前必须了解的硬性约束原文档用专门注释说明并且全部能在 test_api.py 中找到对应测试不能覆盖 Polars 内置命名空间如.str、.dt一旦尝试将抛出AttributeError。Polars 在 api.py#L23-L26 模块加载时即用四个类的_accessors类变量求并集构建了_reserved_namespaces保留名单各目标类的内置集合分别定义于Expr 的_accessorsarr、bin、cat、dt、ext、list、meta、name、str、structSeries 的_accessorsarr、bin、cat、dt、ext、list、plot、str、structDataFrame 的_accessorsplot、styleLazyFrame 的_accessors当前为空集。测试test_namespace_cannot_override_builtin_namespace就以dt为例对四种注册函数逐一断言会抛出AttributeError。不能覆盖已有方法、属性或以下划线开头的私有名。即使该名字不在保留名单中只要类上已存在同名函数isfunction、属性property或以下划线开头同样抛出AttributeError。测试test_namespace_cannot_override_method_or_property分别验证了 DataFrame 的head方法、Series 的dtype属性、Expr 的__or__dunder三种冲突情形。允许覆盖其他自定义命名空间但会产生UserWarning。即第二次用同一名字注册时不会失败只是告警提醒旧实现已被覆盖见 api.py#L57-L65对应测试test_namespace_warning_on_override。此外注册的类若想被 Sphinx 文档正确索引一般把方法实现与其 API 文档写在类体内正如 api.py 中各注册函数 docstring 内嵌的 doctest 示例那样。Expr 命名空间为表达式追加打招呼能力Expr 命名空间是最常用的注册目标因为方法最终要返回pl.Expr才能在.select/.with_columns中参与构建。以下示例来自原文档把greetings命名空间挂到pl.all()之上实现hello/goodbye两种字符串前缀拼接。import polars as pl pl.api.register_expr_namespace(greetings) class Greetings: def __init__(self, expr: pl.Expr) - None: self._expr expr def hello(self) - pl.Expr: return (pl.lit(Hello ) self._expr).alias(hi there) def goodbye(self) - pl.Expr: return (pl.lit(Sayōnara ) self._expr).alias(bye) pl.DataFrame(data[world, world!, world!!]).select( [ pl.all().greetings.hello(), pl.all().greetings.goodbye(), ] )输出两列分别来自hello与goodbyeshape: (3, 2) ┌───────────────┬──────────────────┐ │ hi there ┆ bye │ │ --- ┆ --- │ │ str ┆ str │ ╞═══════════════╪══════════════════╡ │ Hello world ┆ Sayōnara world │ │ Hello world! ┆ Sayōnara world! │ │ Hello world!! ┆ Sayōnara world!! │ └───────────────┴──────────────────┘要点构造函数的expr: pl.Expr参数由描述符自动注入——你在调用处从不显式传它只需把主对象暂存到self._expr。方法内部用pl.lit、运算符、.alias等常规表达式 API 组合新逻辑因为返回值仍是pl.Expr所以可继续嵌入任何表达式上下文。测试用例 test_custom_expr_namespace 给出了一个更实用的求某数的下一个 / 上一个 / 最近 2 的幂版本power命名空间含log、ceil、floor、round与cast(pl.Int64)的组合可作为参考模式。DataFrame 命名空间把一帧拆成按行奇偶的两帧DataFrame 命名空间适合封装作用于整张表的变换。原文档的split示例先用with_row_index增加行号列再按行号奇偶分别filter出两个子帧最后drop掉临时行号列返回list[pl.DataFrame]import polars as pl pl.api.register_dataframe_namespace(split) class SplitFrame: def __init__(self, df: pl.DataFrame) - None: self._df df def by_alternate_rows(self) - list[pl.DataFrame]: df self._df.with_row_index(namen) return [ df.filter((pl.col(n) % 2) 0).drop(n), df.filter((pl.col(n) % 2) ! 0).drop(n), ] pl.DataFrame( data[aaa, bbb, ccc, ddd, eee, fff], schema[(txt, pl.String)], ).split.by_alternate_rows()输出为两帧组成的列表[┌─────┐ ┌─────┐ │ txt │ │ txt │ │ --- │ │ --- │ │ str │ │ str │ ╞═════╡ ╞═════╡ │ aaa │ │ bbb │ │ ccc │ │ ddd │ │ eee │ │ fff │ └─────┘, └─────┘]可以自由决定命名空间方法的返回类型。返回pl.DataFrame或list[pl.DataFrame]都合法只要返回的是 Polars 对象后续调用方仍能继续使用标准 API例如对返回值逐个.collect()或继续链式处理。LazyFrame 命名空间保持惰性、延迟到 collectLazyFrame 命名空间的价值在于方法内部全程只做查询计划层面的变换.with_columns、.select等不触发实际计算真正的执行推迟到外层.collect()。原文档示例把Int8 / Int16 / Int32三列统一提升为Int64import polars as pl pl.api.register_lazyframe_namespace(types) class DTypeOperations: def __init__(self, ldf: pl.LazyFrame) - None: self._ldf ldf def upcast_integer_types(self) - pl.LazyFrame: return self._ldf.with_columns( pl.col(tp).cast(pl.Int64) for tp in (pl.Int8, pl.Int16, pl.Int32) ) ldf pl.DataFrame( data{a: [1, 2], b: [3, 4], c: [5.6, 6.7]}, schema[(a, pl.Int16), (b, pl.Int32), (c, pl.Float32)], ).lazy() ldf.types.upcast_integer_types().collect()输出a、b由i16 / i32提升为i64浮点列c保持不变shape: (2, 3) ┌─────┬─────┬─────┐ │ a ┆ b ┆ c │ │ --- ┆ --- ┆ --- │ │ i64 ┆ i64 ┆ f32 │ ╞═════╪═════╪═════╡ │ 1 ┆ 3 ┆ 5.6 │ │ 2 ┆ 4 ┆ 6.7 │ └─────┴─────┴─────┘注意调用ldf.types.upcast_integer_types()时并不计算必须显式接.collect()或放入pl.collect_all(...)才能看到结果——这正是 LazyFrame 命名空间与 DataFrame 命名空间在语义上的本质差别。从源码看LazyFrame._accessors目前为空集意味着惰性帧上还没有任何内置命名空间注册冲突只会来自同名方法/属性因此自由度更高。测试 test_custom_lazy_namespace 展示了按列 dtype 分组、用collect_schema().dtypes()动态切分 LazyFrame 的进阶写法。Series 命名空间单列的快捷计算Series 命名空间适用于无法表达成列运算表达式、天然以单列为单位的便捷方法。原文档的math命名空间提供平方与立方import polars as pl pl.api.register_series_namespace(math) class MathShortcuts: def __init__(self, s: pl.Series) - None: self._s s def square(self) - pl.Series: return self._s * self._s def cube(self) - pl.Series: return self._s * self._s * self._s s pl.Series(n, [1, 2, 3, 4, 5]) s2 s.math.square().rename(n2) s3 s.math.cube().rename(n3)得到两个新 Seriesshape: (5,) shape: (5,) Series: n2 [i64] Series: n3 [i64] [ [ 1 1 4 8 9 27 16 64 25 125 ] ]实测中若要把派生 Series 重新放回 DataFrame通常如示例所示用.rename()给出新列名测试 test_custom_series_namespace 验证了浮点输入下s.math.square()的正确性如1.5² 2.25。源码级原理解析装饰器工厂、描述符与保留名单本机制并不依赖魔法方法拦截而是类属性描述符 注册表的组合逻辑集中在 api.py建议精读 L23-L71。第一步构建保留名单。模块导入期执行_reserved_namespaces: set[str] set.union( *(cls._accessors for cls in (pl.DataFrame, pl.Expr, pl.LazyFrame, pl.Series)) )由于四个类在各自源码中声明了_accessors: ClassVar[set[str]]见上文_accessors列表因此 Polars 已注册的内置命名空间会被一次性并入保留名单从根本上阻止用户破坏.str、.dt等官方 API。第二步_create_namespace做冲突仲裁并挂载属性。注册时按序检查name in _reserved_namespaces→ 抛AttributeError保留命名空间类上已存在同名属性且为函数 / property / 下划线开头名 → 抛AttributeError类上已存在同名属性通常是此前自定义的命名空间→ 发UserWarning后继续通过setattr(cls, name, NameSpace(name, ns_class))挂载描述符并把名字加入cls._accessors。第三步NameSpace描述符负责注入主对象。核心实现为 api.py#L32-L45class NameSpace(Generic[NS]): def __init__(self, name: str, namespace: type[NS]) - None: self._accessor name self._ns namespace def __get__(self, instance, cls): if instance is None: return self._ns ns_instance self._ns(instance) setattr(instance, self._accessor, ns_instance) return ns_instance当代码访问expr.greetings时Python 会命中类上的这个非数据描述符并回调__get__首参instance就是当前的Expr/DataFrame等主对象于是用self._ns(instance)实例化你的自定义类把主对象传进__init__随后setattr(instance, self._accessor, ns_instance)把新实例缓存在具体实例上后续重复访问将直接命中实例属性、跳过再次实例化。instance is None类级访问时则直接返回自定义类本身。另外Expr、Series等类使用了自定义元类_Meta(type)例如 expr.py#L140来拦截属性访问以给出友好的移除告警由于注册是在类上显式 setattr动态属性可正常参与解析与元类__getattr__并不冲突。与 Expression / IO 插件的定位差异polars.api的注册命名空间机制与官方文档中另一条扩展路径——Expression / IO 插件——定位不同二者可以共存命名空间注册本文主题纯 Python 层组织能力把若干既有 API 调用按领域语义打包成易读的属性式接口。零编译、零 ABI适合库的 API 外观设计。Expression 插件通过 polars.plugins.register_plugin_function含plugin_path、function_name、is_elementwise、changes_length、returns_scalar、cast_to_supertype等参数把编译好的 Rust 函数注册进 Polars 引擎。仓库内pyo3-polars目录即提供配套开发工具链示例与入门说明见 pyo3-polars 以及用户指南的 expr_plugins.md。IO 插件用polars.io.plugins.register_io_source把自定义文件格式注册为扫描/读取源参考 io_plugins.md。一个常见架构是Rust 侧用 Expression 插件实现需要极致执行效率的叶子函数Python 侧再用命名空间注册把这些函数与常用表达式编排成领域友好的高层接口——两者配合即可做到外部 API 优雅、内部执行高效。工程化注意点注册是一次性进程级副作用。装饰器在模块 import 时执行使用方必须先 import 定义了装饰类的模块df.namespace.method()才能生效。库作者应把注册类放进用户 import 即会加载的包入口并在文档中明确。命名空间名字要具语义且避开关键词冲突。参考保留名单选择名称避免str、dt、list、plot、head、dtype等不同领域可用不同前缀如finance_*、geo_*防止生态内互相覆盖引发的UserWarning。构造与返回类型必须自洽。构造函数参数类型决定描述符注入什么Expr/DataFrame/LazyFrame/Series 对应四种方法返回类型应保持与宿主 API 组合兼容Expr 命名空间尽量返回pl.ExprLazyFrame 命名空间保持惰性返回pl.LazyFrame。静态类型提示需要兜底。动态添加的属性不在类型 stub 中测试代码普遍使用# type: ignore[attr-defined]参见 test_api.py标注这类访问避免 mypy 等工具报错test_class_namespaces_are_registered则会遍历类的属性与_accessors集合校验每个注册的命名空间都被正确记录。行为契约有回归测试兜底。命名冲突、覆盖告警、四类对象的注册与调用行为都已有自动化断言见 test_api.py 的 7 个用例复刻该机制或改造时应同步维护这些契约。结语polars.api的四个注册装饰器把 Polars 的扩展点从函数提升到了命名空间层面既规避了子类化带来的类型割裂又保留了完整的表达式组合、惰性查询与链式调用体验。理解 api.rst 的四个示例、api.py 的描述符实现与 test_api.py 的边界测试即可在自己的库中安全地设计出风格统一、行为可预期的领域扩展接口。【免费下载链接】polarsExtremely fast Query Engine for DataFrames, written in Rust项目地址: https://gitcode.com/GitHub_Trending/po/polars创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考