杰瑞科技汇

Python protobuf如何高效使用?

目录

  1. 什么是 Protocol Buffers?
  2. 环境准备:安装必要的工具
  3. 第一步:定义你的数据结构 (.proto 文件)
  4. 第二步:生成 Python 代码
  5. 第三步:在 Python 中使用生成的代码
    • 创建和填充消息对象
    • 序列化 (Serialization) - 转换为二进制格式
    • 反序列化 (Deserialization) - 从二进制格式还原
  6. 高级特性
    • 处理不同数据类型
    • 嵌套消息和包
    • 枚举
    • oneof (字段互斥)
    • AnyOneof 的现代替代方案
  7. 与 JSON 交互
  8. 最佳实践

什么是 Protocol Buffers?

Protocol Buffers (简称 Protobuf) 是 Google 开发的一种与语言无关、与平台无关的可扩展序列化机制,它类似于 XML、JSON,但更小、更快、更简单。

核心优势:

  • 高效:生成的序列化数据非常紧凑,序列化和反序列化速度极快。
  • 强类型.proto 文件定义了严格的数据结构,编译后会生成强类型的类,减少了运行时错误。
  • 向前/向后兼容:这是 Protobuf 的一个杀手级特性,你可以轻松地在你的数据结构中添加或删除字段,而不会破坏依赖于旧格式的已编译程序,旧程序可以优雅地忽略新字段,新程序也能正确读取旧数据中存在的字段。

环境准备:安装必要的工具

你需要两个主要工具:protoc (Protocol Buffer 编译器) 和 protobuf Python 库。

安装 protoc 编译器

你需要从 Protocol Buffers 的 GitHub Releases 页面 下载 protoc

Windows:

  1. 下载 protoc-<version>-win64.zip
  2. 解压到某个目录,C:\tools\protoc
  3. 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 中所示,你可以在一个消息内部定义另一个消息 (PhoneNumberPerson 内部),生成的 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)

最佳实践

  1. 字段编号 (Field Numbers):

    • 115 之间的编号占用一个字节进行编码,将这些编号分配给最常用或最基本的消息字段。
    • 162047 之间的编号占用两个字节。
    • 为将来可能添加的预留字段保留一些编号(5, 10, 15, 20...)。
    • 一旦发布,就永远不要更改或重用字段编号!
  2. 包管理:

    • 对于大型项目,将 .proto 文件组织到目录结构中,并使用 package 关键字来避免命名冲突。
    • 生成的 Python 类会根据文件路径和 package 关键字形成模块化的结构。
  3. 保持 .proto 文件版本控制:

    • .proto 文件与你的代码一起进行版本控制,这是实现向后兼容性的基础。
  4. 使用 .pyi 文件:

    • 始终使用 --pyi_out 选项生成类型存根文件,这能为你的 IDE 提供强大的类型检查和自动补全功能,大大提高开发效率和代码质量。
  5. 性能:

    • Protobuf 的主要优势在于性能,如果你发现序列化/反序列化很慢,首先检查是否正确使用了 SerializeToStringParseFromString,而不是其他更慢的文本格式。

在 Python 中使用 Protocol Buffers 的完整流程如下:

  1. 安装: 安装 protoc 编译器和 protobuf Python 库。
  2. 定义: 编写 .proto 文件,用 Protobuf 语法定义你的数据结构。
  3. 编译: 运行 protoc 命令,将 .proto 文件编译成 *_pb2.py Python 代码。
  4. 使用: 在 Python 代码中 import 生成的类,像操作普通 Python 对象一样创建、填充、访问字段。
  5. 序列化/反序列化: 使用 SerializeToString() 将对象转换为二进制数据,使用 ParseFromString() 从二进制数据还原对象。
  6. (可选) JSON 交互: 使用 json_format 模块在 Protobuf 消息和 JSON 之间进行转换。

Protobuf 是构建高性能、可维护的微服务、API 和数据存储系统的绝佳选择,希望这份指南能帮助你快速上手!

分享:
扫描分享到社交APP
上一篇
下一篇