PDEP-9: 允许第三方项目通过标准 API 注册 pandas 连接器
- 创建日期: 2023 年 3 月 5 日
- 状态: 已拒绝
- 讨论: #51799 #53005
- 作者: Marc Garcia
- 修订版本: 1
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_period
和 DataFrame.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 连接器没有标准 API,用户需要单独学习每一个。由于 pandas 的 I/O API 使用 read/to 而不是 read/write 或 from/to,存在不一致性,开发人员在许多情况下会忽略这个惯例。此外,即使开发人员遵循 pandas 的惯例,命名空间也会不同,因为连接器的开发人员很少会将他们的函数猴子补丁到
pandas
或DataFrame
命名空间中。 - 使用第三方 I/O 连接器导出数据时,方法链不可行,除非作者对
DataFrame
类进行猴子补丁,这是不应鼓励的。
本文档提议以标准方式向第三方库开放 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 注册表,并为它们在 pandas
、pandas.DataFrame
和 pandas.Series
命名空间中动态创建方法。只有名称以 reader_
或 writer_
开头的 entrypoints 会被 pandas 处理,并且在 entrypoint 中注册的函数将在相应的 pandas 命名空间中对 pandas 用户可用。关键字 reader_
和 writer_
后面的文本将用作函数名称。在上面的示例中,entrypoint 名称 reader_duckdb
将创建 pandas.read_duckdb
。名称为 writer_hive
的 entrypoint 将创建方法 DataFrame.to_hive
和 Series.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_lance
或 DataFrame.to_lance
)。但如果有一个基于 Arrow2 Rust 实现的新 csv
读取器,指南可以建议使用 csv_arrow2
来创建 pandas.read_csv_arrow2
等。
参数的存在和命名。由于许多连接器可能会提供类似的功能,例如只加载数据中的一部分列或处理路径,因此将制定参数的存在和命名指南。对连接器开发者的建议示例可以是
columns
:使用此参数允许用户加载一部分列。允许列表或元组。path
:如果数据集是磁盘上的文件,请使用此参数。允许字符串、pathlib.Path
对象或文件描述符。对于字符串对象,允许自动下载 URL、自动解压缩压缩文件等。可以推荐特定的库以便更轻松、更一致地处理这些情况。schema
:对于没有 schema 的数据集(例如csv
),允许提供 Apache Arrow schema 实例,如果未提供,则自动推断类型。
请注意,以上仅为说明性指南示例,并非指南本身的提案,指南将在本 PDEP 批准后独立制定。
连接器注册和文档。为了简化连接器的发现及其文档查找,鼓励连接器开发者将他们的项目注册到集中位置,并使用标准的文档结构。这将允许创建一个统一的网站,用于查找可用的连接器及其文档。它还将允许为特定实现定制文档,并包含其最终 API。
连接器示例
本节列出了可以立即从本提案中受益的连接器的具体示例。
PyArrow 目前提供 Table.from_pandas
和 Table.to_pandas
。使用新接口,它也可以注册 DataFrame.from_pyarrow
和 DataFrame.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())
Polars、Vaex 以及其他 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 连接器。
限制
本提案的实施有一些限制,在此讨论
- 缺乏对多引擎的支持。当前的 pandas I/O API 支持同一格式(对于同一函数或方法名称)的多个引擎。例如
read_csv(engine='pyarrow', ...)
。支持引擎要求特定格式的所有引擎使用相同的签名(相同的参数),这并不理想。不同的连接器可能会有不同的参数,并且使用*args
和**kwargs
会给用户带来更复杂和困难的体验。因此,本提案倾向于函数和方法名称是唯一的,而不是支持引擎选项。 - 缺乏对连接器类型检查的支持。本 PDEP 提议动态创建函数和方法,并且使用 stub 文件无法对它们进行类型检查。对于 pandas 中其他动态创建的组件来说,情况已经如此,例如自定义访问器。
- 不对当前 I/O API 进行改进。在对本提案的讨论中,曾考虑改进当前的 pandas I/O API,以修复使用
read
/to
(而不是例如read
/write
)的不一致性,避免对非 I/O 操作使用to_
前缀方法,或为连接器使用专用命名空间(例如DataFrame.io
)。所有这些更改都不在本 PDEP 的范围之内。
未来计划
本 PDEP 仅用于为现有或未来的连接器提供更好的 API 支持。修改 pandas 代码库中现有连接器不在本 PDEP 的范围之内。
与此 PDEP 相关的未来讨论的一些想法包括
-
导入 pandas 时自动加载 I/O 插件。
-
从 pandas 代码库中移除一些使用频率最低的连接器,例如 SAS、SPSS 或 Google BigQuery,并将它们移至通过此接口注册的第三方连接器。
-
讨论 pandas 连接器的更好 API。例如,使用
read_*
方法代替from_*
方法,重命名未用作 I/O 连接器的to_*
方法,使用一致的术语,例如 from/to、read/write、load/dump 等,或为连接器使用专用命名空间(例如pandas.io
而不是通用的pandas
命名空间)。 -
将
DataFrame
构造函数支持的一些格式实现为 I/O 连接器。
PDEP-9 历史
- 2023 年 3 月 5 日: 初始版本
- 2023 年 5 月 30 日: 重大重构,以使用 pandas 现有 API 和 dataframe 交换 API,并让用户明确加载插件
- 2023 年 6 月 13 日: 经过多次迭代后,该 PDEP 未获得支持,作者已将其关闭并标记为已拒绝