PDEP-9: 允许第三方项目通过标准 API 注册 pandas 连接器

PDEP 摘要

本文档提议,实现 pandas I/O 或内存连接器的第三方项目可以使用 Python 的 entrypoint 系统注册它们,并通过通常的 pandas I/O 接口向 pandas 用户提供这些连接器。例如,独立于 pandas 的包可以实现读取 DuckDB 的读取器和写入 Delta Lake 的写入器,当安装在用户环境中时,用户将能够像使用 pandas 中实现的连接器一样使用它们。例如

import pandas

pandas.load_io_plugins()

df = pandas.DataFrame.read_duckdb("SELECT * FROM 'my_dataset.parquet';")

df.to_deltalake('/delta/my_dataset')

这将允许轻松扩展现有连接器的数量,增加对新格式、数据库引擎、数据湖技术、外核连接器、新的 ADBC 接口等的支持,同时降低 pandas 代码库的维护成本。

当前状态

pandas 支持使用 I/O 连接器从不同格式导入和导出数据,这些连接器当前实现在 pandas/io 中,同时也支持连接到内存结构,例如 Python 结构或其他库格式。在许多情况下,这些连接器包装了现有的 Python 库,而在其他一些情况下,pandas 实现了读写特定格式的逻辑。

在某些情况下,同一格式存在不同的引擎。使用这些连接器的 API 是使用 pandas.read_<format>(engine='<engine-name>', ...) 导入数据,以及使用 DataFrame.to_<format>(engine='<engine-name>', ...) 导出数据。

对于导出到内存的对象(例如 Python 字典),API 与 I/O 相同,使用 DataFrame.to_<format>(...)。对于从内存对象导入的格式,API 不同,使用 from_ 前缀代替 read_,即 DataFrame.from_<format>(...)

在某些情况下,pandas API 提供了 DataFrame.to_* 方法,这些方法不用于将数据导出到磁盘或内存对象,而是用于转换 DataFrame 的索引:DataFrame.to_periodDataFrame.to_timestamp

连接器的依赖项默认不加载,并在使用连接器时导入。如果未安装依赖项,将引发 ImportError

>>> pandas.read_gbq(query)
Traceback (most recent call last):
  ...
ImportError: Missing optional dependency 'pandas-gbq'.
pandas-gbq is required to load data from Google BigQuery.
See the docs: https://pandas-gbq.readthedocs.io.
Use pip or conda to install pandas-gbq.

支持的格式

格式列表可以在I/O 指南中找到。接下来将展示一个更详细的表格,包括内存对象和 DataFrame styler 中的 I/O 连接器

格式 读取器 写入器 引擎
CSV X X c, python, pyarrow
FWF X c, python, pyarrow
JSON X X ujson, pyarrow
HTML X X lxml, bs4/html5lib (参数 flavor)
LaTeX X
XML X X lxml, etree (参数 parser)
剪贴板 X X
Excel X X xlrd, openpyxl, odf, pyxlsb (每个引擎支持不同的文件格式)
HDF5 X X
Feather X X
Parquet X X pyarrow, fastparquet
ORC X X
Stata X X
SAS X
SPSS X
Pickle X X
SQL X X sqlalchemy, dbapi2 (从 con 参数的类型推断)
BigQuery X X
dict X X
records X X
string X
markdown X
xarray X

在撰写本文时,io/ 模块包含近 100,000 行 Python、C 和 Cython 代码。

关于何时将格式包含在 pandas 中没有客观标准,上面的列表主要是开发人员对在 pandas 中实现特定格式的连接器感兴趣的结果。

可用于使用 pandas 处理的数据的现有格式数量不断增加,pandas 即使对流行格式也很难保持更新。可能有必要添加连接器到 PyArrow、PySpark、Iceberg、DuckDB、Hive、Polars 和许多其他格式。

同时,如2019 年用户调查所示,有些格式不常使用。这些不太流行的格式包括 SPSS、SAS、Google BigQuery 和 Stata。请注意,调查中仅包含 I/O 格式(不包括 records 或 xarray 等内存格式)。

支持所有格式的维护成本不仅在于维护代码和评审拉取请求,还在于持续集成系统安装依赖项、编译代码、运行测试等方面花费了大量时间。

在某些情况下,某些连接器的主要维护者不属于 pandas 核心开发团队,而是专注于其中一种格式的人。

提案

虽然当前的 pandas 方法运行良好,但很难找到一个稳定的解决方案,其中 pandas 产生的维护成本不会太大,同时用户可以轻松直观地与他们感兴趣的所有不同格式和表示形式交互。

第三方包已经能够实现连接 pandas 的连接器,但存在一些限制

本文档提议以标准方式向第三方库开放 pandas I/O 连接器的开发,这种标准方式克服了上述限制。

提案实现

实施此提案不需要对 pandas 进行重大更改,并且将使用接下来定义的 API。

用户 API

用户将能够使用标准的包管理工具(pip、conda 等)安装实现 pandas 连接器的第三方包。这些连接器应实现 entrypoints,pandas 将使用这些 entrypoints 自动创建相应的方法 pandas.read_*pandas.DataFrame.to_*pandas.Series.to_*。此接口不会创建任意函数或方法名称,只允许 read_*to_* 模式。

只需安装相应的包并调用函数 pandas.load_io_plugins(),用户就能使用如下代码

import pandas

pandas.load_io_plugins()

df = pandas.read_duckdb("SELECT * FROM 'dataset.parquet';")

df.to_hive(hive_conn, "hive_table")

此 API 允许方法链

(pandas.read_duckdb("SELECT * FROM 'dataset.parquet';")
       .to_hive(hive_conn, "hive_table"))

I/O 函数和方法的总数预计会很少,因为用户通常只使用一小部分格式。如果将不太流行的格式(例如 SAS、SPSS、BigQuery 等)从 pandas 核心移除到第三方包中,数量实际上可能会减少。迁移这些连接器不属于本提案的一部分,可以在随后的单独提案中讨论。

插件注册

第三方包将实现在entrypoints 中定义它们实现的连接器,这些 entrypoints 属于 dataframe.io 组。

例如,一个假设的项目 pandas_duckdb 实现了一个 read_duckdb 函数,可以使用 pyproject.toml 定义以下 entry point

[project.entry-points."dataframe.io"]
reader_duckdb = "pandas_duckdb:read_duckdb"

当用户调用 pandas.load_io_plugins() 时,它将读取 dataframe.io 组的 entrypoint 注册表,并为它们在 pandaspandas.DataFramepandas.Series 命名空间中动态创建方法。只有名称以 reader_writer_ 开头的 entrypoints 会被 pandas 处理,并且在 entrypoint 中注册的函数将在相应的 pandas 命名空间中对 pandas 用户可用。关键字 reader_writer_ 后面的文本将用作函数名称。在上面的示例中,entrypoint 名称 reader_duckdb 将创建 pandas.read_duckdb。名称为 writer_hive 的 entrypoint 将创建方法 DataFrame.to_hiveSeries.to_hive

不以 reader_writer_ 开头的 entrypoints 将被此接口忽略,但不会引发异常,因为它们可以用于此 API 的未来扩展或其他相关的 dataframe I/O 接口。

内部 API

连接器将使用 dataframe 交换 API 向 pandas 提供数据。当从连接器读取数据时,并在将其作为对 pandas.read_<format> 的响应返回给用户之前,数据将从数据交换接口解析并转换为 pandas DataFrame。实际上,连接器很可能会返回一个 pandas DataFrame 或一个 PyArrow Table,但该接口将支持实现 dataframe 交换 API 的任何对象。

连接器指南

为了向用户提供更好、更一致的体验,将制定指南以统一术语和行为。接下来定义了一些需要统一的主题。

避免名称冲突的指南。由于预计某些格式会存在多个实现,正如现在已经发生的那样,将制定如何命名连接器的指南。如果预计会存在多个连接器,最简单的方法可能是将格式命名为 to_<format>_<implementation-id> 类型的字符串。例如,对于 LanceDB,可能只存在一个连接器,并且可以使用名称 lance(这将创建 pandas.read_lanceDataFrame.to_lance)。但如果有一个基于 Arrow2 Rust 实现的新 csv 读取器,指南可以建议使用 csv_arrow2 来创建 pandas.read_csv_arrow2 等。

参数的存在和命名。由于许多连接器可能会提供类似的功能,例如只加载数据中的一部分列或处理路径,因此将制定参数的存在和命名指南。对连接器开发者的建议示例可以是

请注意,以上仅为说明性指南示例,并非指南本身的提案,指南将在本 PDEP 批准后独立制定。

连接器注册和文档。为了简化连接器的发现及其文档查找,鼓励连接器开发者将他们的项目注册到集中位置,并使用标准的文档结构。这将允许创建一个统一的网站,用于查找可用的连接器及其文档。它还将允许为特定实现定制文档,并包含其最终 API。

连接器示例

本节列出了可以立即从本提案中受益的连接器的具体示例。

PyArrow 目前提供 Table.from_pandasTable.to_pandas。使用新接口,它也可以注册 DataFrame.from_pyarrowDataFrame.to_pyarrow,这样当 PyArrow 安装在环境中时,pandas 用户就可以使用他们习惯的接口来使用转换器。在#51760 中讨论了与 PyArrow 表的更好集成。

当前 API:

pyarrow.Table.from_pandas(table.to_pandas()
                               .query('my_col > 0'))

提议的 API:

(pandas.read_pyarrow(table)
       .query('my_col > 0')
       .to_pyarrow())

PolarsVaex 以及其他 dataframe 框架可以从第三方项目中受益,这些项目使得与 pandas 的互操作使用更明确的 API。在#47368 中有人请求与 Polars 集成。

当前 API:

polars.DataFrame(df.to_pandas()
                   .query('my_col > 0'))

提议的 API:

(pandas.read_polars(df)
       .query('my_col > 0')
       .to_polars())

DuckDB 提供一个外核引擎,能够在加载数据之前下推谓词,从而更好地利用内存并显著减少加载时间。pandas 由于其急切(eager)的特性,本身不容易实现这一点,但可以从 DuckDB 加载器中受益。加载器已经可以在 pandas 内部实现(已在#45678 中提出),或者作为具有任意 API 的第三方扩展。但本提案将允许创建具有标准且直观 API 的第三方扩展。

pandas.read_duckdb("SELECT *
                    FROM 'dataset.parquet'
                    WHERE my_col > 0")

外核算法将过滤或分组等操作推送到数据加载阶段。虽然目前尚不可能实现,但可以使用此接口开发实现外核算法的连接器。

大数据系统,例如 Hive、Iceberg、Presto 等,可以从将数据加载到 pandas 的标准方式中受益。此外,可以将查询结果作为 Arrow 返回的常规 SQL 数据库,将受益于比基于 SQL Alchemy 和 Python 结构的现有连接器更好、更快的连接器。

任何其他格式,包括领域特定格式,都可以轻松实现具有清晰直观 API 的 pandas 连接器。

限制

本提案的实施有一些限制,在此讨论

未来计划

本 PDEP 仅用于为现有或未来的连接器提供更好的 API 支持。修改 pandas 代码库中现有连接器不在本 PDEP 的范围之内。

与此 PDEP 相关的未来讨论的一些想法包括

PDEP-9 历史