当前位置: 首页 > news >正文

用 Python 玩转 Protocol Buffers(基于 edition=2023)

1. 为什么是 Protocol Buffers(protobuf)

在 Python 里,序列化结构化数据有很多方法,但都各有短板:

  • Pickle:内置方便,但跨语言差模式演进差(升级字段容易崩)。
  • 自定义串格式:比如把 4 个 int 拼成 "12:3:-23:67",轻量但需要手写编码/解析,后期成本高,只适合非常简单的数据。
  • XML:人类可读、语言绑定多,但体积大、编解码慢,DOM 访问也麻烦。

Protocol Buffers 兼具高效二进制自动代码生成良好演进性三大优势:
你写好 .protoprotoc 就会生成 Python 类;这些类支持快速编码/解析,并能在不破坏兼容的前提下逐步扩展数据结构。

2. 示例目标与目录结构

我们实现一个“地址簿”小程序,完成两件事:

  • 将用户输入写入地址簿文件(protobuf 二进制);
  • 从文件中读取所有人的信息并打印,顺便演示 JSON 序列化与反序列化。

建议项目结构:

project/
├─ proto/
│  └─ addressbook.proto
├─ gen/                     # 生成的 Python 代码输出目录
│  └─ addressbook_pb2.py
├─ add_person.py
├─ list_people.py
└─ requirements.txt

requirements.txt(如果需要 JSON 转换):

protobuf>=4.25

3. 定义协议:addressbook.proto

本文示例使用 editionsedition = "2023")。它代表了一组语言/编译选项的组合,便于随着时间演进。

edition = "2023";package tutorial;message Person {string name = 1;int32  id   = 2;string email = 3;enum PhoneType {PHONE_TYPE_UNSPECIFIED = 0;PHONE_TYPE_MOBILE = 1;PHONE_TYPE_HOME   = 2;PHONE_TYPE_WORK   = 3;}message PhoneNumber {string number = 1;PhoneType type = 2 [default = PHONE_TYPE_HOME];}repeated PhoneNumber phones = 4;
}message AddressBook {repeated Person people = 1;
}

要点速览

  • package:避免跨项目命名冲突(Python 最终模块路径由目录决定,但 package 仍有意义,且对其他语言有效)。
  • 标签号(tag)= 1, 2, ... 是字段在二进制里的唯一标识。1–15 编码更省空间,优先留给高频或 repeated 字段。
  • repeated:可出现任意次(含 0),并保序,可视作动态数组。
  • 不支持类继承:protobuf 通过组合与嵌套来建模。

4. 生成 Python 代码

4.1 安装 protoc

从官方发布页获取你的平台版本,按 README 安装。Python 依赖:

pip install protobuf

4.2 一条命令生成

protoc --proto_path=proto \--python_out=gen \proto/addressbook.proto

可选:生成类型注解存根(便于 IDE 补全)
--pyi_out=gen 会额外生成 addressbook_pb2.pyi

完成后,gen/addressbook_pb2.py 即可在 Python 中导入使用(注意 PYTHONPATH 或以包形式引用)。

5. Python 端生成物到底是什么?

与 Java/C++ 不同,Python 生成器不会直接把所有字段访问方法写死在源码里。它生成的是:

  • 描述符(descriptors):记录消息、枚举、字段的“结构化元信息”;
  • “空类”定义:为每个消息创建一个类,然后由
  • 元类 GeneratedProtocolMessageType模块加载时读取描述符,动态注入真正的方法与属性(如 SerializeToString()、字段访问等)。

因此你可以像普通对象一样操作字段,但如果你:

  • 设置了 不存在 的字段 → AttributeError
  • 赋了 错误类型 的值 → TypeError
  • 读取 未设置 的字段 → 得到类型默认值(字符串空、数值 0、布尔 false)

示例:

import addressbook_pb2 as abp = ab.Person()
p.id = 1234
p.name = "John Doe"
p.email = "jdoe@example.com"
phone = p.phones.add()
phone.number = "555-4321"
phone.type = ab.Person.PHONE_TYPE_HOME

6. 写入示例:交互创建并持久化

文件:add_person.py

#!/usr/bin/env python3
import sys
sys.path.append("gen")        # 让 Python 找到生成文件;也可改为包导入
import addressbook_pb2 as abdef prompt_for_person(person: ab.Person) -> None:person.id = int(input("Enter person ID number: "))person.name = input("Enter name: ")email = input("Enter email address (blank for none): ").strip()if email:person.email = emailwhile True:number = input("Enter a phone number (or leave blank to finish): ").strip()if not number:breakphone = person.phones.add()phone.number = numberphone_type = input("Is this a mobile, home, or work phone? ").strip().lower()if   phone_type == "mobile": phone.type = ab.Person.PHONE_TYPE_MOBILEelif phone_type == "home":   phone.type = ab.Person.PHONE_TYPE_HOMEelif phone_type == "work":   phone.type = ab.Person.PHONE_TYPE_WORKelse:print("Unknown phone type; using default (HOME).")def main():if len(sys.argv) != 2:print("Usage:", sys.argv[0], "ADDRESS_BOOK_FILE")sys.exit(2)path = sys.argv[1]book = ab.AddressBook()# 读取已有文件(若不存在则新建)try:with open(path, "rb") as f:book.ParseFromString(f.read())except IOError:print(f"{path}: Could not open file. Creating a new one.")prompt_for_person(book.people.add())with open(path, "wb") as f:f.write(book.SerializeToString())print(f"Saved to {path}")if __name__ == "__main__":main()

运行:

python add_person.py addressbook.bin

7. 读取示例:遍历并打印

文件:list_people.py

#!/usr/bin/env python3
import sys
sys.path.append("gen")
import addressbook_pb2 as abdef list_people(book: ab.AddressBook) -> None:for person in book.people:print("Person ID:", person.id)print("  Name:", person.name)# edition=2023/proto2 中,可对可选字段使用 HasFieldif person.HasField("email"):print("  E-mail address:", person.email)for phone in person.phones:kind = {ab.Person.PHONE_TYPE_MOBILE: "Mobile",ab.Person.PHONE_TYPE_HOME:   "Home",ab.Person.PHONE_TYPE_WORK:   "Work",}.get(phone.type, "Unknown")print(f"  {kind} phone #: {phone.number}")def main():if len(sys.argv) != 2:print("Usage:", sys.argv[0], "ADDRESS_BOOK_FILE")sys.exit(2)book = ab.AddressBook()with open(sys.argv[1], "rb") as f:book.ParseFromString(f.read())list_people(book)if __name__ == "__main__":main()

8. JSON 读写:json_format 一把梭

有时候你需要把 protobuf 与 JSON 互转(便于调试或与 Web 组件对接):

from google.protobuf import json_format
import addressbook_pb2 as abperson = ab.Person(id=1234, name="John Doe", email="jdoe@example.com")# 序列化为 JSON 字符串
json_str = json_format.MessageToJson(person)# 从 JSON 解析回消息对象
parsed = ab.Person()
json_format.Parse(json_str, parsed)

注意:SerializeToString() / ParseFromString() 面向的是二进制;JSON 互转需使用 json_format

9. 标准消息方法速查

  • IsInitialized():检查 required 字段是否齐全(更常见于 proto2 风格)。
  • __str__():可读字符串(print(msg) 时触发)。
  • CopyFrom(other):用另一条消息整体覆盖。
  • Clear():清空到初始状态。

10. 设计建议:封装(组合)而非继承

protobuf 生成类本质是数据载体(像 C 的 struct),并不适合作为拥有复杂行为的一等领域对象。
最佳实践:写一个应用层封装类,内部组合 protobuf 消息对象,并在封装类中实现业务行为与校验。

  • ✅ 组合(wrap):隐藏细节、提供便捷方法、可演进
  • ❌ 继承(inherit):会破坏生成类内部机制,也不符合 OOP 最佳实践

11. 协议演进的“硬规则”(保持新旧兼容)

当你需要升级 .proto

  1. 不要修改既有字段的 tag
  2. 不要添加或删除任何 required 字段;
  3. 可以删除 optional / repeated 字段;
  4. 可以新增 optional / repeated 字段,但必须使用从未使用过的 tag(被删字段用过的编号也不能复用)。

遵守后:

  • 旧代码能读新消息,并忽略未知字段
  • 新代码能读旧消息;
  • 新增的可选字段在旧消息中不存在:你需要在代码里处理类型默认值(字符串空、布尔 false、数值 0);
  • 新增的 repeated 字段没有 HasField,新代码无法区分“留空”与“从未设置过”。

12. 常见坑位与排查

  • 导入失败:确保把 gen/ 加入 PYTHONPATH,或把它做成包(加 __init__.py)。
  • bytes vs str:二进制 API 用 bytes;不要误把 str 直接喂给 ParseFromString()
  • HasField 的语义:对消息字段与 proto2/edition 可选字段有效;对 repeated 与部分标量(proto3 默认)无效。
  • tag 规划:1–15 更省空间,优先给高频/重复字段;永远不要复用历史 tag
  • 类型约束:赋错类型会 TypeError,不要忽略。

13. 一键跑通清单(Checklist)

  • 安装 protocprotobufpip install protobuf
  • 写好 proto/addressbook.proto(合理分配 tag)
  • 运行 protoc --proto_path=proto --python_out=gen proto/addressbook.proto
  • 运行 python add_person.py addressbook.bin;输入若干人
  • 运行 python list_people.py addressbook.bin;确认输出
  • 如需联调 Web/CLI,使用 json_format 做 JSON 互转

文章转载自:

http://sEzW5VZl.qnzLd.cn
http://LnnIzqN8.qnzLd.cn
http://JMByDYiK.qnzLd.cn
http://zopOaUZB.qnzLd.cn
http://dZMEamkx.qnzLd.cn
http://XNZbQMgz.qnzLd.cn
http://CDeAJOYM.qnzLd.cn
http://TJ1FwDOQ.qnzLd.cn
http://k0OEvhQJ.qnzLd.cn
http://GX3ocww8.qnzLd.cn
http://1WWF3yzv.qnzLd.cn
http://enUQeVUy.qnzLd.cn
http://nrhqMxuS.qnzLd.cn
http://BwSTnRMg.qnzLd.cn
http://fY68SgIq.qnzLd.cn
http://7QfnL3NV.qnzLd.cn
http://wWOIPTqe.qnzLd.cn
http://QJpRwsNm.qnzLd.cn
http://ScjNVJln.qnzLd.cn
http://DMWmbcdE.qnzLd.cn
http://soTGQsGH.qnzLd.cn
http://xyOpHRUz.qnzLd.cn
http://Yy1zSpYV.qnzLd.cn
http://dX1AK7Ca.qnzLd.cn
http://NgX5wTQH.qnzLd.cn
http://KjQyExAa.qnzLd.cn
http://TbLOTZtJ.qnzLd.cn
http://YzkDGGia.qnzLd.cn
http://L0iwuz7v.qnzLd.cn
http://NIasCnnN.qnzLd.cn
http://www.dtcms.com/a/385403.html

相关文章:

  • 配置文件和动态绑定数据库(上)
  • 整体设计 之 绪 思维导图引擎 之 引 认知系统 之 序 认知元架构 之 认知科学的系统级基础设施 框架 之1
  • AI办公革命:企业微信如何成为智能办公中枢?
  • 企业微信AI功能实操指南:智能表格与邮件如何提升协作效率?
  • 04 完成审批任务
  • keil出现 cmsis_compiler.h(279): error: #35: #error directive: Unknown compilr解决方法
  • CSS `:has()` 实战指南:让 CSS 拥有“if 逻辑”
  • 【开题答辩全过程】以 Java校园二手书城平台为例,包含答辩的问题和答案
  • 机器视觉在新能源汽车电池中有哪些检测应用
  • CES Asia的“五年计划”:打造与北美展比肩的科技影响力
  • 王梦迪团队推出TraceRL:迈向扩散语言模型「RL大一统」
  • 运用脚本部署lamp架构
  • Springboot项目中引入ES(一)
  • 专项智能练习(认知主义学习理论)
  • Mysql索引总结(1)
  • Spring Boot中的Binder类基本使用和工具封装
  • 数字化工厂建设:是简单组装PLM/ERP/MES/WMS等系统,还是彻底重构?
  • 带你了解STM32:OLED调试器
  • 软考中项考几门?多少分合格?
  • 1.5 调用链分层架构 - mybatis源码学习
  • 线性代数 · 矩阵 | 秩 / 行秩 / 列秩 / 计算方法
  • 期权时间价值会增长么?
  • 数据结构(陈越,何钦铭) 第十讲 排序(下)
  • Java——JVM
  • 【51单片机】【protues仿真】基于51单片机温度检测系统
  • 51单片机-使用IIC通信协议实现EEPROM模块教程
  • ISP Pipeline
  • Tomcat的安装和启动步骤以及常见问题
  • 基于 Selenium+Page Object 的电商平台自动化测试框架实践
  • 内网安全:自签名、CA机构签发与SSH、sudo最佳实践