Protobuf-py:专为Python打造的Protocol Buffers库,毫不妥协
摘要
Buf 宣布推出 protobuf-py,一个从头编写的全新 Python Protocol Buffers 库,通过了完整的兼容性测试套件,生成可读的类型化 Python 代码,并提供可选的 Rust 加速以提高性能。
暂无内容
查看缓存全文
缓存时间: 2026/07/12 10:47
# protobuf-py: 为 Python 打造的、毫无妥协的 Protobuf · Buf
来源:https://buf.build/blog/protobuf-py
今天我们发布了 `protobuf-py` (https://github.com/bufbuild/protobuf-py),一个完全从头开始编写的 **Protocol Buffers** (https://protobuf.dev/) Python 库。它通过了 Protobuf 一致性测试套件 (https://github.com/protocolbuffers/protobuf/tree/main/conformance) 中的每一个二进制和 JSON 用例,覆盖 proto2、proto3 和 editions,并且支持扩展、自定义选项、未知字段、动态消息和 well-known 类型。它生成可读的、带类型注解的 Python 代码,没有运行时依赖,且仅依赖纯 Python 3.10+。如果安装了其 Rust 加速器,那么对于生产工作负载,它的速度与 Google 的 Python 包 (https://pypi.org/project/protobuf/) 所运行的 C 引擎 upb (https://github.com/protocolbuffers/protobuf/tree/main/upb) 一样快。
对于 Python 开发者来说,过去的选择一直是在一个完整的 Protobuf 实现和一个感觉像 Python 的库之间进行取舍。Google 的包很完整,但其 API 深受 C++ 和 Java 的影响。`betterproto` (https://github.com/betterproto/python-betterproto2) 用起来很舒服,但它牺牲了太多规范内容。`protobuf-py` 则能同时为 Python 开发者提供这两者。
## 为什么要构建另一个 Protobuf 运行时?
Python 对于 Protobuf 而言太重要了,以至于它不该让人感觉像是事后才想到的东西。它广泛应用于数据管道、机器学习系统、AI 代理、基础设施脚本、RPC 服务和开发者工具中。然而,在 Python 中使用 protobuf 的体验并不符合开发者对如此流行语言的期望。Google 的包完整且经受过实战考验,但其 API 和生成的代码仍然感觉像是其他语言运行时的绑定。`betterproto` 证明了 Python 开发者想要更优雅的东西,但它从未实现完整的规范。`grpcio` 给 RPC 层带来了同样的问题:它功能强大、使用广泛,但很难围绕它进行构建。
我们最初的目标是使用 connect-py (https://github.com/connectrpc/connect-py)(一个同时支持 Connect 和 gRPC 的 **ConnectRPC** (https://connectrpc.com/) 实现)来修复 RPC 层。但仅有传输层是不够的。一个好的 RPC 栈仍然依赖于底层的消息,而 Python 当时缺少我们想要作为基础的 Protobuf 运行时。我们想要一个足够完整以处理真实 schema、足够可读以供日常 Python 使用、并且足够快速以至于没人需要为选择它而道歉的运行时。
`protobuf-py` 就是问询这些约束能否同时成立的结果。它是一个为 Python 构建的完整实现,提供了我们期望 `connect-py` 所依赖的规范覆盖、生成代码和性能特征,同时既不需要封装 Google 的运行时,也不需要削减规范内容。
## 为什么 Google 的包会有这样的体验
从 PyPI 安装 `protobuf`,你通常得到的引擎是 `upb`,用 C 语言编写。你的消息存在于一个 C arena 中,而 Python 对象只是进入该 arena 的句柄。读取一个字段会进入 C,找到值,然后在返回的路上构造一个 Python 对象。
这是一种在多种语言之间共享引擎的好方法,但它留下了无处不在的痕迹:
- 生成的 `_pb2.py` 文件难以阅读,因为其中几乎没什么 Python 代码可读。类在导入时被组装以配置 C 引擎,因此“转到定义”只会看到一串序列化的描述符字节。
- `SerializeToString`、`HasField`、`WhichOneof` 和 `CopyFrom` 这些 API 是为 C++ 的易用性而设计的。在 proto3 标量上调用 `HasField` 会引发异常。`WhichOneof` 返回一个字符串,然后你需要把它传给 `getattr` 来获取它已经定位到的值。
- 生成的导入是绝对路径,一旦你将它们嵌套在包中就会失效。PyPI 上存在一个名为 `fix-protobuf-imports` 的单独工具,仅用于重写 Google 的输出。
- 类型注册在一个进程全局的池中,因此导入同一个 `.proto` 的两个不同构建版本会在运行时引发异常。
这些都不是绑定层中的随机缺陷。它们是当 Python API 主要为 C++/Java 代码库中的一致性而设计,而非作为一个地道的 Python 包时,所必然产生的结果。笨拙的 API 和速度是打包在一起的,多年来你只能要么全要,要么什么都不要。
## 我们构建了什么替代方案
`protobuf-py` 将你的消息保留在 Python 中。它是一个带有 `__slots__` 的普通对象,其字段是普通的 Python 值:整数、字符串、列表、子消息。一个 Rust 加速器会加速需要的操作(主要是解析和序列化),并将结果直接写入对象中。解析完成后,读取一个字段就只是访问一个 Python 属性。
由于数据是 Python 的,生成的代码也是真正的代码。`protoc-gen-py` 会发出一个你可以阅读的类:
```python
class User(Message[_UserFields]):
__slots__ = ("first_name", "last_name", "active", "manager", "locations", "projects", "contact")
if TYPE_CHECKING:
def __init__(
self,
*,
first_name: str = "",
last_name: str = "",
active: bool = False,
manager: User | None = None,
locations: list[str] | None = None,
projects: dict[str, str] | None = None,
contact: Oneof[Literal["email"], str] | Oneof[Literal["phone"], str] | None = None,
) -> None: ...
first_name: str
last_name: str
active: bool
manager: User | None
locations: list[str]
projects: dict[str, str]
contact: Oneof[Literal["email"], str] | Oneof[Literal["phone"], str] | None
```
使用它看起来就像使用语言中的任何其他东西一样:
```python
import copy
from gen.user_pb import User
from protobuf import Oneof
user = User(first_name="Alice", active=True, locations=["NYC", "LDN"])
user.last_name = "Smith"
wire = user.to_binary()
parsed = User.from_binary(wire)
match parsed.contact:
case Oneof(field="email", value=email):
send(email)
case Oneof(field="phone", value=phone):
call(phone)
inactive = copy.replace(parsed, active=False) # Python 3.13+
```
Oneof 变成了你可以进行模式匹配的值,类型检查器会收窄每个分支。枚举是真正的 `IntEnum` 成员。pyright、mypy 和 ty 在没有 stubs 包的情况下就能读懂的生成输出。生成的文件使用相对导入,可以放在你喜欢的任何地方。类型通过显式的 `Registry` 而非全局池来解析。这一切都源于将消息数据保留在 Python 中。
## 它很完整,不是一个“友好的子集”
其他 Protobuf 库比 Google 的更容易使用,但通常为了达到这一点而丢弃了一半的规范。`betterproto` 是目前最令人愉快的选择,但它只支持 proto3,不支持 proto2、editions、扩展或自定义选项。
`protobuf-py` 覆盖了整个规范。它处理 proto2、proto3、editions、扩展、自定义选项、groups、跨往返的未知字段保留、packed 和 unpacked 重复字段,以及 well-known 类型的完整 ProtoJSON 编码。它通过了 Google 用于认证其自身运行时的一致性测试套件,在二进制和 JSON 方面没有失败。一个空的失败列表已经检入仓库,如果失败列表不再为空,CI 会失败。
## 在关键之处保持快速
一个解析消息然后丢弃的基准测试会让 `upb` 看起来不可战胜,因为它将工作推迟到你读取时。生产代码会解析一次消息,然后根据字段进行分支,提取出几个值,复制消息,并将修改后的版本序列化回去。
`upb` 在每一次读取时都要付出 Python 转换的成本。`protobuf-py` 在第一次读取后就无需再付出这个成本,因为解析已经产生了一个普通的 Python 对象。成本在边界处流向另一方。将数据保留在 Python 中意味着 `protobuf-py` 前期做了更多工作。当代码对消息执行了足够多的操作以赚回这些时间时,这种付出就会得到回报。
这里我们将两个包放在一个真实世界的例子中进行对比:为某个社交媒体网站的用户主页构建响应,同时还有原始的编组步骤(单独进行)。数字是相对于 `upb` 的吞吐量(每秒操作数),越高越快。
| 工作负载 | upb | `protobuf-py` |
|---|---|---|
| 构建首页响应(端到端) | 1.0x | **1.06x** |
| ├ 解析(单独) | 1.0x | 0.22x |
| └ 序列化(单独) | 1.0x | 0.60x |
单独来看,`upb` 在编组方面胜出,完全符合预期。但在端到端测试中,运行服务在生产环境中实际会执行的代码,`protobuf-py` 领先了。每次 `upb` 需要将字段读取转换回 Python 时,`protobuf-py` 都早已完成了这一步,在整个请求过程中,这累计起来足以产生比 C 运行时更快的运行速度。
这里的负载是重文本型(完整的 Reddit 帖子内容、多句子的个人简介和通知),这是社交、文档以及 LLM/Agent 流量的真实情景。在这种情况下,将字符串保留为 Python 对象获得的收益最大。你可以在 `test_bench.py` (https://github.com/bufbuild/protobuf-py/blob/main/packages/bench/tests/test_bench.py) 中自行运行测试工具。
Rust 加速器是可选的并且是自动的。你不需要 Rust 工具链来使用 `protobuf-py`,贡献者也不需要。预编译的 wheel 会在存在时加载它,在不存在时纯 Python 路径的行为完全相同。它可以在 free-threaded 的 3.14 构建上运行,并且该包没有运行时依赖。
## 我们以前就做过这些
Buf (https://buf.build/) 多年来一直在修复 Protobuf 中那些迫使团队为其工具编写脚本的部分:Buf CLI (https://github.com/bufbuild/buf)、Buf Schema Registry (https://buf.build/product/bsr)、ConnectRPC (https://connectrpc.com/)、Protovalidate (https://protovalidate.com/) 以及用于 TypeScript 的 protobuf-es (https://github.com/bufbuild/protobuf-es)。Buf 的工程师还参与了 Protobuf editions 的工作,因此 `protobuf-py` 是由帮助编写规范的人编写的。它属于同一传统。
## 尝试一下
```bash
# 在你现有的 UV 项目中
uv add protobuf-py
uv add --dev protoc-gen-py buf-bin
```
将一个 `buf.gen.yaml` 指向你的 `.proto` 文件:
```yaml
version: v2
inputs:
- directory: proto
plugins:
- local: protoc-gen-py
out: gen
```
然后生成:
```bash
uv run buf generate
```
你会得到一个带类型的 `_pb.py` 文件,可以直接打开阅读。文档 (https://protobufpy.com/) 已经上线,源代码和基准测试框架在 GitHub 上,Issues 也已经开放。告诉我们你使用得如何!
相似文章
Python 3.14 直接编译为机器码 – 无需解释器
pon 是一个用 Rust 编写的 Python 3.14 新 JIT 和提前编译器,它直接将 Python 编译为机器码,无需解释器或字节码,并使用垃圾回收器替代引用计数。
Pyrefly v1.0 正式发布
Pyrefly,一款开源 Python 类型检查器和语言服务器,现已发布 v1.0,标志着其达到生产就绪状态,同时带来了显著的性能提升,并被 PyTorch 和 NumPy 等主要代码库所采用。
Symbolica 2.0: 面向Python和Rust的可编程符号
Symbolica 2.0是面向Python和Rust的符号计算框架的重大版本,引入了可编程符号、改进的Rust API、更丰富的输出格式(HTML、Typst、彩色)、用于求值的JIT编译以及更好的易用性。
@ErickSky:他们用 Rust 完全重写了 PostgreSQL……而且已经 100% 通过了官方的 Postgres 测试!注意,这是……
从头用 Rust 重新实现 PostgreSQL,已 100% 通过官方测试,磁盘兼容,并声称在开发中版本中性能大幅提升(事务型负载快 50%,分析型负载快约 300 倍)。
用Rust重写的Postgres,现已100%通过Postgres回归测试
pgrust是用Rust重写的PostgreSQL 18.3,通过了所有46k+回归测试,旨在通过Rust和AI辅助使Postgres更易于修改。它是磁盘兼容的,并且可以从现有的Postgres数据目录启动。