系列
:Python 提效工具 100 篇 · 第 022 篇
版本:pyarrow 23.0.1 · Python ≥ 3.10
难度:⭐⭐⭐
关键词:Arrow、列式内存、Parquet、Dataset、Pandas、DuckDB
你以为自己在学文件格式,其实是在补一层“数据总线”
很多人第一次接触 PyArrow,都是因为 Parquet。
场景通常是这样的:
- 再给 DuckDB、Spark、Polars 或别的系统继续处理
表面上你在折腾的是“文件格式”,真正一直在消耗时间和内存的,却是数据在不同工具之间反复搬运、反复拷贝、反复丢类型信息。
PyArrow 真正值钱的地方,不是会写 Parquet,而是它给 Python 世界补上了一层统一的列式数据中间层。
这层中间层一旦建立起来:
- DuckDB、Polars、数据湖工具链之间更容易互通
- 大文件、多文件、分区目录的扫描不必每次都先塞回 DataFrame
- schema、类型、空值、批次这些本来容易混乱的细节,终于有了更稳定的表达方式
如果你最近正在做本地分析、离线报表、日志巡检、Parquet 数据集、AI 数据预处理,PyArrow 值得认真补上。
PyArrow 是什么
PyArrow 是 Apache Arrow 的 Python 库。官方一句话定位是:Python library for Apache Arrow。
但只看这句话太轻了。更准确的理解应该是:
PyArrow 是 Python 里访问 Arrow 列式内存格式、Parquet、Dataset 和高性能列式计算能力的核心入口。
你可以把 Arrow 理解成一套“跨工具、跨语言的列式数据标准件”:
- Array
- Table
- RecordBatch
- Schema
- Parquet / IPC / Feather
- Dataset
PyArrow 本身不等于 Pandas,也不等于 DuckDB。
- Pandas 强在 DataFrame 交互式清洗和序列运算
- DuckDB 强在 SQL 聚合、JOIN、窗口函数
PyArrow、Pandas、DuckDB 到底怎么分工
一句话判断:
- 你要洗数据、改列、画图
- 你要把数据在多个系统之间顺畅传递
- 你要做聚合、JOIN、窗口函数、报表 SQL
安装
pip install pyarrow
pip install pyarrow pandas # 如果你要演示 DataFrame 互转
PyArrow 轮子比较大,但价值也集中在这里:底层能力不是“纯 Python 凑出来的”,而是直接把 Arrow 生态的核心能力带进 Python。
3 分钟上手:先别急着想 DataFrame,先理解 Table
很多人一上来就想“PyArrow 怎么替代 DataFrame”。
这就是第一层误区。
PyArrow 不该先拿来和 DataFrame 正面硬拼,而应该先理解它的核心数据结构——Table。
import pyarrow as pa
schema = pa.schema([
("episode", pa.string()),
("duration_s", pa.int32()),
("score", pa.float64()),
])
table = pa.Table.from_pydict(
{
"episode": ["ep001", "ep002"],
"duration_s": [3200, 2800],
"score": [9.1, 8.6],
},
schema=schema,
)
print(table.schema)
print(table.to_pylist())
输出里最关键的不是数据本身,而是这两件事:
- schema 是明确的
- 列式结构是稳定的,后续写 Parquet、转 Pandas、喂给别的工具都会更顺
五个最值得掌握的核心能力
1. Array、Table、RecordBatch、Schema:先建立 Arrow 直觉
最少要先理解这四个词:
pa.array(...)pa.Table.from_*pa.RecordBatch.from_*pa.schema(...)
import pyarrow as pa
names = pa.array(["Alice", "Bob", "Carol"])
scores = pa.array([92, 81, 88])
batch = pa.RecordBatch.from_arrays([names, scores], names=["name", "score"])
table = pa.Table.from_batches([batch])
print(batch.num_rows)
print(table.column_names)
为什么这层直觉很重要?
因为 PyArrow 的很多能力——Parquet、Dataset、跨工具互转、批次扫描——本质上都不是围着“一个胖 DataFrame 对象”转,而是围着这些列式对象转。
2. Table.from_pandas() / to_pandas():和 Pandas 互转,但别盲目迷信“零拷贝”
import pandas as pd
import pyarrow as pa
source_df = pd.DataFrame(
{
"episode": ["ep001", "ep002", "ep003"],
"lang": ["zh", "en", "zh"],
"duration_s": [3200, 2800, 1500],
}
)
arrow_table = pa.Table.from_pandas(source_df)
restored_df = arrow_table.to_pandas()
print(arrow_table.schema)
print(restored_df.head())
这一组 API 很常用,但要记住两个边界:
- 不是所有转换都天然零拷贝。
to_pandas() 甚至提供了 zero_copy_only=True,意思正说明“能不能零拷贝,要看数据类型和布局是否满足条件”。 object dtype 是类型信息最容易模糊的地方。官方文档也明确提醒:object 列往往需要猜类型;如果 DataFrame 为空,或者列里全是 None,类型推断就可能退化成 null。
所以更稳的工程写法通常是:
- 真要拿 Arrow 当数据中间层时,尽量少让模糊
object 列到处飘
3. pyarrow.parquet:Parquet 不只是“压缩存盘”,而是列式数据的自然落地形式
import pyarrow as pa
import pyarrow.parquet as pq
summary = pa.table(
{
"episode": ["ep001", "ep002", "ep003"],
"size_mb": [80.5, 68.2, 77.1],
"status": ["done", "done", "queued"],
}
)
pq.write_table(summary, "episodes.parquet", compression="snappy")
restored = pq.read_table("episodes.parquet", columns=["episode", "size_mb"])
print(restored.to_pylist())
write_table() / read_table() 是最常见的入口。
这里最值得记住的不是“会写文件”,而是三个判断:
- Parquet 是列式存储
- 读 Parquet 时可以按列读、按条件筛,天然更适合分析型负载
- PyArrow 直接掌握 Parquet 的底层结构,因此它往往是 Python 里处理 Parquet 最直接的一层
官方文档里还能看到 version、compression、row_group_size 等参数。
最实用的经验是:
- 默认先用
compression="snappy" - 如果你更看重生态兼容性,Parquet
version 不要随便追新
4. pyarrow.dataset:真正把 PyArrow 价值拉开的,是多文件与分区目录
如果你只把 PyArrow 当成“单文件读写工具”,那还是低估了它。
pyarrow.dataset 才是它在工程里真正值钱的一层。
官方文档明确把它定位为:高效处理表格型、可能大于内存、而且往往是多文件的数据集。
import pyarrow as pa
import pyarrow.dataset as ds
partition_schema = pa.schema([
("day", pa.string()),
("lang", pa.string()),
])
source = pa.table(
{
"day": ["2026-04-06", "2026-04-06", "2026-04-07"],
"lang": ["zh", "en", "zh"],
"episode": ["ep001", "ep002", "ep003"],
"size_mb": [80.5, 68.2, 77.1],
}
)
ds.write_dataset(
source,
base_dir="episode_dataset",
format="parquet",
partitioning=["day", "lang"],
partitioning_flavor="hive",
)
dataset = ds.dataset(
"episode_dataset",
format="parquet",
partitioning=ds.partitioning(partition_schema, flavor="hive"),
)
filtered = dataset.to_table(
filter=(ds.field("day") == "2026-04-06") & (ds.field("lang") == "zh"),
columns=["episode", "size_mb"],
)
print(filtered.to_pylist())
这段代码里真正关键的是:
- 多文件目录被当成一个逻辑数据集来处理
- 分区字段可以直接参与过滤
- 扫描阶段就能做列投影和条件过滤
这就是官方文档反复强调的几个词:
- potentially larger than memory
5. pyarrow.compute:在列式对象上直接做高性能计算
很多人只记得 PyArrow 会读写文件,却忘了它还有 compute 模块。
import pyarrow as pa
import pyarrow.compute as pc
durations = pa.array([3200, 2800, None, 1500])
mask = pc.greater(durations, 2000)
filtered = pc.filter(durations, mask)
total = pc.sum(filtered)
print(filtered)
print(total.as_py())
compute 适合做这类事情:
但也要有边界感。
如果你已经开始想:
那就说明你该往 DuckDB 走了。PyArrow 不是不能算,而是它最强的价值通常不是把自己写成 SQL 引擎,而是把列式数据准备好,交给更合适的下游。
一个真实应用例子:播客素材元数据分区数据集
假设你每天都会产出一批播客素材元数据,字段包括:
如果继续用 CSV 散着放,会出现几个问题:
- 想给 DuckDB、Pandas、下游脚本复用时,入口不统一
这时很适合让 PyArrow 先把数据层整理干净。
from pathlib import Path
import pandas as pd
import pyarrow as pa
import pyarrow.dataset as ds
records = pd.DataFrame(
{
"day": ["2026-04-06", "2026-04-06", "2026-04-07", "2026-04-07"],
"lang": ["zh", "en", "zh", "zh"],
"episode": ["ep001", "ep002", "ep003", "ep004"],
"duration_s": [3200, 2800, 0, 3500],
"size_mb": [80.5, 68.2, 0.0, 95.3],
"status": ["done", "done", "failed", "done"],
"qc_score": [9.2, 8.7, 2.0, 9.5],
}
)
schema = pa.schema(
[
("day", pa.string()),
("lang", pa.string()),
("episode", pa.string()),
("duration_s", pa.int32()),
("size_mb", pa.float64()),
("status", pa.string()),
("qc_score", pa.float64()),
]
)
table = pa.Table.from_pandas(records, schema=schema, preserve_index=False)
base_dir = Path("podcast_arrow_dataset")
ds.write_dataset(
table,
base_dir=str(base_dir),
format="parquet",
partitioning=["day", "lang"],
partitioning_flavor="hive",
existing_data_behavior="delete_matching",
)
partition_schema = pa.schema([
("day", pa.string()),
("lang", pa.string()),
])
dataset = ds.dataset(
str(base_dir),
format="parquet",
partitioning=ds.partitioning(partition_schema, flavor="hive"),
)
ready = dataset.to_table(
filter=(ds.field("status") == "done") & (ds.field("lang") == "zh"),
columns=["day", "episode", "duration_s", "size_mb", "qc_score"],
)
print(ready.to_pandas())
这套写法的价值在于:
- 入口统一
- 分区明确
- 类型稳定:schema 写死,后面不会“这次是 float,下次又被猜成 string”
- 上下游更顺:想继续给 Pandas、DuckDB、报表脚本都方便
生产实践里最容易踩的几个点
1. 不要把 object dtype 当成理所当然
Pandas 的 object 列在工程里经常是“先能跑再说”的产物。
但到了 Arrow 世界,类型系统会更严谨。
如果你让 PyArrow 去猜:
它就可能推断出你并不想要的结果。
关键字段尽量显式 schema。
2. 不要把“零拷贝”当成宣传口号去乱写
PyArrow 确实能显著减少数据在不同工具间的重复拷贝,但“零拷贝”从来不是无条件成立。
真正稳的表述应该是:
- Arrow 的列式布局让零拷贝或低拷贝变得更有可能
- 但是否真的零拷贝,要看具体类型、内存布局和目标工具是否兼容
- 如果你需要严格保证,应该看 API 是否提供显式约束,例如
zero_copy_only=True
3. Dataset 适合扫描,不代表它要取代 SQL 引擎
pyarrow.dataset 非常适合:
但如果你的需求已经演化成:
那就别硬拧,直接接 DuckDB 更顺。
4. Parquet 参数先保守,再优化
pyarrow.parquet.write_table() 支持非常多参数:
compressionversionrow_group_sizeuse_dictionarywrite_statistics
如果你没有明确基准测试,先用稳妥策略:
- 版本优先兼容性,不盲目追最新 logical type
- row group 调优要建立在真实读取模式上,不要拍脑袋
5. PyArrow 不是用来替代所有上层工具的
PyArrow 最适合作为:
它不是你所有分析逻辑都要手写在里面的地方。
什么时候应该优先想到 PyArrow
如果只能记住一句话:
PyArrow 最值得学的,不是“又一个数据处理库”,而是它把 Python 数据工作流里最容易断裂的地方接起来了。
SVG 配图说明
本文三张信息图均为自制 SVG(可缩放矢量图形)格式,再转为 PNG 内嵌。SVG 本质上是用代码描述图形的矢量格式,放大不糊、后期易改,特别适合技术文章里的流程图、结构图和对比图。
小结
PyArrow 的价值,不在于它会替你做所有分析,而在于它把 列式内存、Parquet、分区数据集、跨工具互通 这几件原本很散的事情,收成了一条清晰的数据通路。
当你的 Python 数据工作流开始跨越 Pandas、Parquet、DuckDB、Polars、数据湖目录时,越早补上 PyArrow,这条链路越稳。
系列索引:Python 提效工具 100 篇路线图