Python的基本Protobuf使用
Python的基本Protobuf使用
协议缓冲区(Protobuf)是Google开发的与语言无关的数据序列化格式。Protobuf之所以出色,原因如下:
- 数据量低: Protobuf使用二进制格式,该格式比JSON等其他格式更紧凑。
- 持久性: Protobuf序列化是向后兼容的。这意味着即使接口在此期间发生了更改,您也可以始终还原以前的数据。
- 按合同设计: Protobuf要求使用显式标识符和类型来规范消息。
- gRPC的要求: gRPC(gRPC远程过程调用)是一种利用Protobuf格式的高效远程过程调用系统。
1. 安装Protobuf
对于大多数系统,Protobuf必须从源代码安装。在下面,我描述了Unix系统的安装:
从Git下载最新的Protobuf版本:
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.12.4/protobuf-all-3.12.4.tar.gz
解压缩
tar -xzf protobuf-all-3.12.4.tar.gz
安装:
cd protobuf-3.12.4/ && ./configure && make && sudo make install
验证安装(protoc
现在应该可用!)
protocprotoc --version
2. Protobuf消息的定义
要使用Protobuf,我们首先需要定义我们要传输的消息。消息在.proto
文件内定义。请考虑官方文档以获取协议缓冲区语言的详细信息。在这里,我仅提供一个简单的示例,旨在展示最重要的语言功能。
假设我们正在开发一个类似Facebook的社交网络,该社交网络完全是关于人及其联系的。这就是为什么我们要为一个人建模消息。
一个人具有某些固有的特征(例如年龄,性别,身高),还具有我们需要建模的某些外在特征(例如朋友,爱好)。让我们存储以下定义src/interfaces/person.proto
:
syntax = "proto3";
import "generated/person_info.proto";
package persons;
message Person {
PersonInfo info = 1; // characteristics of the person
repeated Friend friends = 2; // friends of the person
}
message Friend {
float friendship_duration = 1; // duration of friendship in days
repeated string shared_hobbies = 2; // shared interests
Person person = 3; // identity of the friend
}
请注意,我们引用的是另一个原始文件,generated/person_info.proto
我们将其定义为:
syntax = "proto3";
package persons;
enum Sex {
M = 0; // male
F = 1; // female
O = 2; // other
}
message PersonInfo {
int32 age = 1; // age in years
Sex sex = 2;
int32 height = 3; // height in cm
}
现在将解释最重要的关键字:
- 语法:语法定义了规范使用哪个版本的Protobuf。我们正在使用
proto3
。 - import:如果根据另一条消息定义了一条消息,则需要使用
import
语句将其包括在内。您可能想知道为什么导入person.proto
?我们稍后将对此进行更深入的研究-现在仅知道这是由于Python的导入系统所致。generated/person_info.proto``interfaces/person_info.proto
- package:包定义了属于同一名称空间的消息。这样可以防止名称冲突。
- enum:一个枚举定义一个枚举类型。
- messsage:消息是我们想使用Protobuf建模的一条信息。
- repeat:
repeated
关键字指示一个变量,该变量被解释为向量。在我们的情况下,friends
是Friend
消息的向量。
还要注意,每个消息属性都分配有一个唯一的编号。该编号对于协议的向后兼容是必需的:一旦将编号分配给字段,则不应在以后的时间点对其进行修改。
现在我们有了应用程序的基本原型定义,我们可以开始生成相应的Python代码了。
3. proto原始文件编译
要将原始文件编译为Python对象,我们将使用Protobuf编译器protoc
。
我们将使用以下选项调用原型编译器:
--python_out
:将存储已编译的Python文件的目录--proto_path
:由于原始文件不在项目的根文件夹中,因此我们需要使用替代文件。通过指定generated=./src/interfaces
,编译器知道在导入其他原始消息时,我们要使用生成文件的路径(generated
),而不是接口的位置(src/interfaces
)。
有了这种了解,我们可以像下面这样编译原始文件:
mkdir src/generated
protoc src/interfaces/person_info.proto --python_out src/ --proto_path generated=./src/interfaces/
protoc src/interfaces/person.proto --python_out src/ --proto_path generated=./src/interfaces/
或者:
# python_out用于指定生成python文件
# =.表示生成的python文件在当前目录下
# Person.proto用于编译的proto源文件
protoc --python_out=. person_info.proto
protoc --python_out=. person.proto
执行完这些命令后,文件generated/person_pb2.py
和generated/person_info_pb2.py
应该存在。
(venv-patroni-4.0.3) (venv-patctl-2.1.0) [fbase@localhost protobuf]$ ll
total 16
-rw-rw-r-- 1 fbase fbase 1540 Dec 23 02:18 person_info_pb2.py
-rw-rw-r-- 1 fbase fbase 212 Jan 3 2025 person_info.proto
-rw-rw-r-- 1 fbase fbase 1665 Dec 23 02:19 person_pb2.py
-rw-rw-r-- 1 fbase fbase 389 Jan 3 2025 person.proto
生成的Python代码并非真正可读。但这没关系,因为我们只需要知道person_pb2.py
可以用于构造可序列化的Protobuf对象即可。
4. Protobuf对象的序列化
# fill protobuf objects
import os
import person_pb2 as person_pb2
import person_info_pb2 as person_info_pb2
############
# define friend for person of interest
#############
friend_info = person_info_pb2.PersonInfo()
friend_info.age = 40
friend_info.sex = person_info_pb2.Sex.M
friend_info.height = 165
friend_person = person_pb2.Person()
friend_person.info.CopyFrom(friend_info)
friend_person.friends.extend([]) # no friends :-(
#######
# define friendship characteristics
########
friendship = person_pb2.Friend()
friendship.friendship_duration = 365.1
friendship.shared_hobbies.extend(["books", "daydreaming", "unicorns"])
friendship.person.CopyFrom(friend_person)
#######
# assign the friend to the friend of interest
#########
person_info = person_info_pb2.PersonInfo()
person_info.age = 30
person_info.sex = person_info_pb2.Sex.M
person_info.height = 184
person = person_pb2.Person()
person.info.CopyFrom(person_info)
person.friends.extend([friendship]) # person with a single friend
请注意,我们通过直接分配填充了所有琐碎的数据类型(例如,整数,浮点数和字符串)。仅对于更复杂的数据类型,才需要使用其他一些功能。例如,我们利用extend
来填充重复的Protobuf字段并CopyFrom
填充Protobuf子消息。
要序列化Protobuf对象,我们可以使用SerializeToString()
函数。此外,我们还可以使用以下str()
函数将Protobuf对象输出为人类可读的字符串:
# serialize proto object
import os
from protobuf.test01 import person
out_dir = "/home/fbase/soft/zmq/fbasecman/protobuf"
with open(os.path.join(out_dir, "person.pb"), "wb") as f:
# binary output
f.write(person.SerializeToString())
with open(os.path.join(out_dir, "person.protobuf"), "w") as f:
# human-readable output for debugging
f.write(str(person))
执行完代码段后,可以在proto_dump/person.protobuf
以下位置找到生成的人类可读的Protobuf消息:
info {
age: 30
height: 184
}
friends {
friendship_duration: 365.1
shared_hobbies: "books"
shared_hobbies: "daydreaming"
shared_hobbies: "unicorns"
person {
info {
age: 40
height: 165
}
}
}
请注意,此人的信息既不显示该人的性别,也不显示其朋友的性别。这不是Bug,而是Protobuf的功能:0
永远不会打印值为的条目。sex
由于这两个人都是男性,因此此处未显示0
。
5. 自动化Protobuf编译
在开发过程中,每次更改后必须重新编译原始文件可能会变得很乏味。要在安装开发Python软件包时自动编译原始文件,我们可以使用该setup.py
脚本。
让我们创建一个函数,该函数为.proto
目录中的所有文件生成Protobuf代码src/interfaces
并将其存储在下src/generated
:
import pathlib
import os
from subprocess import check_call
def generate_proto_code():
proto_interface_dir = "./src/interfaces"
generated_src_dir = "./src/generated/"
out_folder = "src"
if not os.path.exists(generated_src_dir):
os.mkdir(generated_src_dir)
proto_it = pathlib.Path().glob(proto_interface_dir + "/**/*")
proto_path = "generated=" + proto_interface_dir
protos = [str(proto) for proto in proto_it if proto.is_file()]
check_call(["protoc"] + protos + ["--python_out", out_folder, "--proto_path", proto_path])
6. 优势
之前,我提到Protobuf的优点之一是其二进制格式。在这里,我们将通过比较Protobuf消息的大小和Person
相应的JSON来考虑此优势:
"person": {
"info": {
"age": 30,
"height": 184
},
"friends": {
"friendship_duration": 365.1000061035156,
"shared_hobbies": ["books", "daydreaming", "unicorns"],
"person": {
"info": {
"age": 40,
"height": 165
}
}
}
}
比较JSON和Protobuf文本表示形式,结果发现JSON实际上更紧凑,因为它的列表表示形式更加简洁。但是,这令人产生误解,因为我们对二进制Protobuf格式感兴趣。
当比较Person
对象的二进制Protobuf和JSON占用的字节数时,我们发现以下内容:
du -b person.pb
53 person.pb
du -b person.json
304 person.json
在这里,Protobuf比JSON小5倍
二进制Protobuf(53个字节)比相应的JSON(304个字节)小5倍以上。请注意,如果我们使用gRPC协议传输二进制Protobuf,则只能达到此压缩级别。
如果不选择gRPC,则常见的模式是使用base64编码对二进制Protobuf数据进行编码。尽管此编码不可撤销地将有效载荷的大小增加了33%,但仍比相应的REST有效载荷小得多。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)