目录
- 什么是 Protocol Buffers?
- 环境准备:安装必要的工具
- 第一步:定义你的数据结构 (
.proto文件) - 第二步:生成 Python 代码
- 第三步:在 Python 中使用生成的代码
- 创建和填充消息对象
- 序列化 (Serialization) - 转换为二进制格式
- 反序列化 (Deserialization) - 从二进制格式还原
- 高级特性
- 处理不同数据类型
- 嵌套消息和包
- 枚举
oneof(字段互斥)Any和Oneof的现代替代方案
- 与 JSON 交互
- 最佳实践
什么是 Protocol Buffers?
Protocol Buffers (简称 Protobuf) 是 Google 开发的一种与语言无关、与平台无关的可扩展序列化机制,它类似于 XML、JSON,但更小、更快、更简单。
核心优势:
- 高效:生成的序列化数据非常紧凑,序列化和反序列化速度极快。
- 强类型:
.proto文件定义了严格的数据结构,编译后会生成强类型的类,减少了运行时错误。 - 向前/向后兼容:这是 Protobuf 的一个杀手级特性,你可以轻松地在你的数据结构中添加或删除字段,而不会破坏依赖于旧格式的已编译程序,旧程序可以优雅地忽略新字段,新程序也能正确读取旧数据中存在的字段。
环境准备:安装必要的工具
你需要两个主要工具:protoc (Protocol Buffer 编译器) 和 protobuf Python 库。
安装 protoc 编译器
你需要从 Protocol Buffers 的 GitHub Releases 页面 下载 protoc。
Windows:
- 下载
protoc-<version>-win64.zip。 - 解压到某个目录,
C:\tools\protoc。 - 将
bin目录(C:\tools\protoc\bin)添加到系统的PATH环境变量中。
macOS (使用 Homebrew):
brew install protobuf
Linux (Debian/Ubuntu):
sudo apt-get update sudo apt-get install protobuf-compiler
安装后,在终端运行 protoc --version 验证是否安装成功。
安装 Python 库
在你的 Python 项目中,安装官方的 protobuf 库。
pip install protobuf
第一步:定义你的数据结构 (.proto 文件)
这是所有工作的起点,创建一个名为 person.proto 的文件,并定义你的消息结构。
// person.proto
// 指定使用 proto3 语法
syntax = "proto3";
// 可选:指定 Python 包名,生成的代码会放在这个包下
option java_package = "com.example.person";
option java_outer_classname = "PersonProto";
// 定义一个消息,类似于 Python 中的类或结构体
message Person {
// 字段格式: "数据类型 字段名 = 字段编号;"
// 字段编号是唯一的,一旦定义就不能改变,即使删除了字段也不能复用。
string name = 1;
int32 age = 2; // 32-bit integer
string email = 3;
// 可以嵌套定义其他消息
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
// repeated 关键字表示这是一个列表/数组
repeated PhoneNumber phones = 4;
}
// 定义一个枚举
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
// 定义另一个消息,引用 Person
message AddressBook {
repeated Person people = 1;
}
关键字解释:
syntax = "proto3";: 声明使用 proto3 语法,这是目前推荐的版本。message: 定义一个消息类型。string,int32,int64,bool,float,double: 基本数据类型。repeated: 表示该字段可以包含 0 个或多个值,相当于 Python 的list。enum: 定义枚举类型。= 1, 2, 3...: 字段的唯一标识符(标签号)。非常重要,用于二进制编码。
第二步:生成 Python 代码
使用 protoc 编译器将你的 .proto 文件转换为 Python 代码。
打开终端,进入 person.proto 文件所在的目录,然后运行以下命令:
# --python_out: 指定输出目录,"." 表示当前目录 # --pyi_out: (推荐) 同时生成 .pyi 类型存根文件,有助于 IDE (如 VS Code, PyCharm) 提供更好的代码提示 # person.proto: 你的 proto 文件 protoc --python_out=. --pyi_out=. person.proto
执行成功后,你会看到生成了两个文件:
person_pb2.py: 包含所有消息类的 Python 实现文件。person_pb2.pyi: 类型提示文件,强烈建议保留。
第三步:在 Python 中使用生成的代码
现在你可以在 Python 脚本中导入并使用这些生成的类了。
# import the generated classes
from person_pb2 import Person, AddressBook, PhoneType
# --- 1. 创建和填充消息对象 ---
# 创建一个 Person 对象
p = Person()
p.name = "Alice"
p.age = 30
p.email = "alice@example.com"
# 添加电话号码 (phones 是一个列表)
phone1 = p.phones.add() # 使用 .add() 方法为 repeated 字段添加元素
phone1.number = "13800138000"
phone1.type = PhoneType.MOBILE
phone2 = p.phones.add()
phone2.number = "010-12345678"
phone2.type = PhoneType.WORK
# 你也可以直接创建嵌套对象
# p.phones.extend([phone1, phone2]) # 也可以用 extend
print("--- Created Person Object ---")
print(f"Name: {p.name}")
print(f"Age: {p.age}")
print(f"Email: {p.email}")
print("Phones:")
for phone in p.phones:
print(f" - {phone.number} (Type: {phone.type})")
print("-" * 20)
# --- 2. 序列化 (Serialization) ---
# 将对象转换为二进制数据 (bytes)
data = p.SerializeToString()
print(f"Serialized data (bytes): {data}")
print(f"Length of serialized data: {len(data)} bytes")
print("-" * 20)
# --- 3. 反序列化 (Deserialization) ---
# 从二进制数据创建一个新的 Person 对象
new_p = Person()
new_p.ParseFromString(data)
print("--- Deserialized Person Object ---")
print(f"Name: {new_p.name}")
print(f"Age: {new_p.age}")
print(f"Email: {new_p.email}")
print("Phones:")
for phone in new_p.phones:
print(f" - {phone.number} (Type: {phone.type})")
print("-" * 20)
输出结果:
--- Created Person Object ---
Name: Alice
Age: 30
Email: alice@example.com
Phones:
- 13800138000 (Type: MOBILE)
- 010-12345678 (Type: WORK)
--------------------
Serialized data (bytes): b'\n\x05Alice\x1a\x1e\n\x0c\x08Alice\x12\x1c\n\x10alice@example.com"\x1c\n\x0b13800138000\x10\x00"\x1e\n\x08010-12345678\x10\x02'
Length of serialized data: 54 bytes
--------------------
--- Deserialized Person Object ---
Name: Alice
Age: 30
Email: alice@example.com
Phones:
- 13800138000 (Type: MOBILE)
- 010-12345678 (Type: WORK)
--------------------
高级特性
处理不同数据类型
| Protobuf 类型 | Python 类型 | 备注 |
|---|---|---|
double |
float |
64-bit floating point |
float |
float |
32-bit floating point |
int32 |
int |
使用变长编码,负数效率低 |
int64 |
int |
使用变长编码 |
uint32 |
int |
无符号 32-bit integer |
uint64 |
int |
无符号 64-bit integer |
sint32 |
int |
有符号整数,负数编码更高效 |
sint64 |
int |
有符号整数,负数编码更高效 |
fixed32 |
int |
总是 4 字节,适合大数值 |
fixed64 |
int |
总是 8 字节,适合大数值 |
sfixed32 |
int |
总是 4 字节的有符号整数 |
sfixed64 |
int |
总是 8 字节的有符号整数 |
bool |
bool |
|
string |
str |
UTF-8 或 7-bit ASCII 编码的文本 |
bytes |
bytes |
任意字节序列 |
嵌套消息和包
如 person.proto 中所示,你可以在一个消息内部定义另一个消息 (PhoneNumber 在 Person 内部),生成的 Python 类是平铺的,但逻辑上仍然是嵌套的。Person.PhoneNumber。
option java_package = "com.example.person"; 这样的选项主要用于 Java,在 Python 中,生成的 person_pb2.py 文件本身就是一个模块。
枚举
枚举在 Python 中会生成一个简单的类,其成员是该枚举值。
# person_pb2.py 会包含类似这样的代码
class PhoneType(enum.IntEnum):
MOBILE = 0
HOME = 1
WORK = 2
# ... 其他方法 ...
你可以直接使用 PhoneType.MOBILE。
oneof (字段互斥)
当一个消息中只有一个字段会被设置时,使用 oneof 可以节省空间并防止逻辑错误。
// search.proto
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
oneof result_type {
string text_result = 4;
string url = 5;
}
}
在 Python 中,oneof 字段的行为很特殊:
from search_pb2 import SearchRequest
s = SearchRequest()
s.query = "python protobuf"
# 设置 oneof 字段
s.text_result = "This is the text content."
print(s.HasField("text_result")) # True
print(s.HasField("url")) # False
# 再次设置 oneof 的另一个字段
s.url = "http://example.com"
print(s.HasField("text_result")) # False (text_result 被清空)
print(s.HasField("url")) # True
# 你可以使用 WhichOneof() 来检查当前设置了哪个字段
print(s.WhichOneof("result_type")) # 'url'
与 JSON 交互
Protobuf 提供了将消息与 JSON 相互转换的便捷方法。
你需要安装 protobuf 的 JSON 支持:
pip install protobuf
(新版本的 protobuf 库通常已经内置了 JSON 支持,无需额外安装包)
from person_pb2 import Person, PhoneType
import json
# 1. 从 Python 对象创建 JSON
p = Person()
p.name = "Bob"
p.age = 25
p.email = "bob@example.com"
# 将消息转换为 JSON 字符串
json_str = json.dumps(p, indent=2)
print("--- JSON from Person ---")
print(json_str)
print("-" * 20)
# 2. 从 JSON 字符串创建 Python 对象
# 注意:JSON 的键名必须与 .proto 文件中的字段名完全一致
json_data = '''
{
"name": "Charlie",
"age": 35,
"email": "charlie@example.com",
"phones": [
{
"number": "13900139000",
"type": "MOBILE"
}
]
}
'''
new_p = Person()
# 使用 json.loads 和 ParseFromString 的组合
new_p.ParseFromString(json.dumps(json_data).encode('utf-8')) # 这种方法太绕了
# 正确的方法是使用 json_format 模块 (推荐)
from google.protobuf import json_format
# 正确的 JSON 反序列化方法
new_p_from_json = json_format.Parse(json_data, Person, ignore_unknown_fields=True)
print("--- Person from JSON ---")
print(f"Name: {new_p_from_json.name}")
print(f"Age: {new_p_from_json.age}")
print(f"Email: {new_p_from_json.email}")
print("Phones:")
for phone in new_p_from_json.phones:
print(f" - {phone.number} (Type: {phone.type})")
print("-" * 20)
最佳实践
-
字段编号 (Field Numbers):
- 在
1到15之间的编号占用一个字节进行编码,将这些编号分配给最常用或最基本的消息字段。 16到2047之间的编号占用两个字节。- 为将来可能添加的预留字段保留一些编号(5, 10, 15, 20...)。
- 一旦发布,就永远不要更改或重用字段编号!
- 在
-
包管理:
- 对于大型项目,将
.proto文件组织到目录结构中,并使用package关键字来避免命名冲突。 - 生成的 Python 类会根据文件路径和
package关键字形成模块化的结构。
- 对于大型项目,将
-
保持
.proto文件版本控制:- 将
.proto文件与你的代码一起进行版本控制,这是实现向后兼容性的基础。
- 将
-
使用
.pyi文件:- 始终使用
--pyi_out选项生成类型存根文件,这能为你的 IDE 提供强大的类型检查和自动补全功能,大大提高开发效率和代码质量。
- 始终使用
-
性能:
- Protobuf 的主要优势在于性能,如果你发现序列化/反序列化很慢,首先检查是否正确使用了
SerializeToString和ParseFromString,而不是其他更慢的文本格式。
- Protobuf 的主要优势在于性能,如果你发现序列化/反序列化很慢,首先检查是否正确使用了
在 Python 中使用 Protocol Buffers 的完整流程如下:
- 安装: 安装
protoc编译器和protobufPython 库。 - 定义: 编写
.proto文件,用 Protobuf 语法定义你的数据结构。 - 编译: 运行
protoc命令,将.proto文件编译成*_pb2.pyPython 代码。 - 使用: 在 Python 代码中
import生成的类,像操作普通 Python 对象一样创建、填充、访问字段。 - 序列化/反序列化: 使用
SerializeToString()将对象转换为二进制数据,使用ParseFromString()从二进制数据还原对象。 - (可选) JSON 交互: 使用
json_format模块在 Protobuf 消息和 JSON 之间进行转换。
Protobuf 是构建高性能、可维护的微服务、API 和数据存储系统的绝佳选择,希望这份指南能帮助你快速上手!
