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

PDEP 摘要

本文档建议,实现 I/O 或内存连接器到 pandas 的第三方项目可以使用 Python 的入口点系统注册它们,并通过通常的 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.

支持的格式

格式列表可以在 IO 指南 中找到。接下来将展示一个更详细的表格,包括内存对象以及 DataFrame 样式器中的 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
字典 X X
记录 X X
字符串 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 格式(不包括记录或 xarray 等内存格式)。

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

在某些情况下,一些连接器的主要维护人员不是 pandas 核心开发团队的成员,而是专门从事某种格式的人员。

建议

虽然当前的 pandas 方法运行得相当好,但很难找到一个稳定的解决方案,既能避免 pandas 的维护成本过高,又能让用户以简单直观的方式与他们感兴趣的所有不同格式和表示形式进行交互。

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

本文件建议以标准方式向第三方库开放 pandas I/O 连接器的开发,以克服这些限制。

提案实现

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

用户 API

用户可以使用标准打包工具(pip、conda 等)安装实现 pandas 连接器的第三方软件包。这些连接器应该实现入口点,pandas 将使用这些入口点自动创建相应的 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 核心移到第三方包中,实际上可以减少当前状态下的数量。移动这些连接器不属于本提案的一部分,可以在以后的单独提案中讨论。

插件注册

第三方包将实现 入口点 来定义它们在 dataframe.io 组下实现的连接器。

例如,假设项目 pandas_duckdb 实现了一个 read_duckdb 函数,可以使用 pyproject.toml 来定义下一个入口点

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

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

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

内部 API

连接器将使用 dataframe 交换 API 向 pandas 提供数据。当从连接器读取数据时,在将其作为对 pandas.read_<format> 的响应返回给用户之前,将从数据交换接口解析数据并将其转换为 pandas DataFrame。在实践中,连接器很可能返回 pandas DataFrame 或 PyArrow 表,但接口将支持实现 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 和其他数据帧框架可以从第三方项目中受益,这些项目使与 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 由于其急切的性质,无法轻松地自行实现这一点,但可以从 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 历史