1. 官方链接:Protocol Buffer Basics: Python
翻译方式:基于有道在线翻译作为初版,人工二次校验。
2. 翻译正文
此教程提供为Python程序员提供了使用协议缓冲区的基本介绍。通过创建一个简单的示例应用,本文向你展示:
- 在.proto文件中,定义消息格式:message formats
- 使用协议缓冲区的编译器
- 使用Python版本的协议缓冲区API读写messages
这不是一个关于在Python中使用协议缓冲区的全面指南。更全面的信息可以参照Protocol Buffer Language Guide (proto2),Protocol Buffer Language Guide (proto3)
问题域
我们将要使用的示例是一个非常简单的“地址簿”应用程序,它可以从文件中读写人们的联系方式。地址簿中的每个人都有姓名、ID、电子邮件地址和联系电话号码。
如何序列化和检索这样的结构化数据?有几种方法可以解决这个问题:
- 使用Python Pickle
- 您可以发明一种特别的方法来将数据项编码为单个字符串—例如将4个整数编码为“12:3:-23:67”
- 将数据序列化为XML
您可以使用协议缓冲区来代替这些选项。协议缓冲区正是解决这个问题的灵活、高效、自动化的解决方案。使用协议缓冲区,您可以编写希望存储的数据结构的.proto描述。从此,协议缓冲区编译器创建一个类,该类使用高效的二进制格式实现协议缓冲区数据的自动编码和解析。生成的类为组成协议缓冲区的字段提供getter和setter,并负责将协议缓冲区作为一个单元进行读写的细节。重要的是,协议缓冲区格式支持随着时间推移扩展格式的想法,使代码仍然可以读取用旧格式编码的数据。
示例代码
示例代码包含在源代码包中,位于“examples”目录下。可在这里下载: 下载链接
定义缓冲协议的格式
要创建地址簿应用程序,需要从.proto文件开始。proto文件中的定义很简单: 为想要序列化的每个数据结构添加一条消息,然后为消息中的每个字段指定名称和类型。下面是定义您的消息的。proto文件,addressbook.proto。
syntax = "proto2";
package tutorial;
message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
optional string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
正如您所看到的,语法类似于c++或Java。让我们检查一下文件的每个部分,看看它做了什么。
- 从一个a package declaration开始:可以帮助不同项目之间的命名冲突
- 开始:message definitions,【1】由a set of typed fields集成【2】message可以嵌套message
- 每个元素上的“= 1”,“= 2”标记标识二进制编码中字段使用的唯一“标记”。标记号1-15比较高的数字需要的编码字节少一个,所以作为一种优化,您可以决定将这些标记用于常用或重复的元素,而将标记16或更高的标记用于不常用的可选元素。重复字段中的每个元素都需要对标记号进行重新编码,因此重复字段特别适合进行这种优化。
每个字段必须用以下修饰符之一进行注释:
- optional: 该字段可设置也不可设置。如果未设置可选字段值,则使用默认值。
- repeated: 该字段可以重复任何次数(包括0次)。重复值的顺序将保存在协议缓冲区中。可以将重复字段看作动态大小的数组。
- required: 必须提供字段的值,否则消息将被视为“未初始化”。序列化未初始化的消息将引发异常。解析未初始化的消息将失败。除此之外,必选字段的行为与可选字段完全相同。注意:Required is Forever1:
您可以在协议缓冲区语言指南中找到编写.proto文件的完整指南——包括所有可能的字段类型。但是,不要去寻找类似于类继承的工具——协议缓冲区不会这样做。
编译你的协议缓冲区
现在你有了.proto,接下来需要做的是生成读取和写入AddressBook(以及Person和PhoneNumber)消息所需的类。要做到这一点,你需要在你的.proto上运行协议缓冲区编译器协议:
- 安装编译器。下载与安装说明见下载链接
- 运行编译器。【1】声明源目录 (应用程序源代码所在的目录-如果不提供值则使用当前目录:SRC_DIR)【2】目标目录(您希望生成的代码存放的目录;通常与$SRC_DIR相同)【3】.proto文件路径
# 使用Python classes,因此使用:python_out
protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto
测试Demo
- proto测试说明demo的组织结构,其中addressbook.proto的内容来自上文
- 对.proto进行编译
protoc -I=/home/yang/workspace/gits/slam/protos/src_protos --python_out=/home/yang/workspace/gits/slam/protos/gen_protos /home/yang/workspace/gits/slam/protos/src_protos/addressbook.proto
## 编译后的部分结果
_PERSON_PHONENUMBER.fields_by_name['type'].enum_type = _PERSON_PHONETYPE
_PERSON_PHONENUMBER.containing_type = _PERSON
_PERSON.fields_by_name['phones'].message_type = _PERSON_PHONENUMBER
_PERSON_PHONETYPE.containing_type = _PERSON
_ADDRESSBOOK.fields_by_name['people'].message_type = _PERSON
DESCRIPTOR.message_types_by_name['Person'] = _PERSON
DESCRIPTOR.message_types_by_name['AddressBook'] = _ADDRESSBOOK
Person = _reflection.GeneratedProtocolMessageType('Person', (_message.Message,), dict(
PhoneNumber = _reflection.GeneratedProtocolMessageType('PhoneNumber', (_message.Message,), dict(
DESCRIPTOR = _PERSON_PHONENUMBER,
__module__ = 'addressbook_pb2'
# @@protoc_insertion_point(class_scope:tutorial.Person.PhoneNumber)
))
,
DESCRIPTOR = _PERSON,
__module__ = 'addressbook_pb2'
# @@protoc_insertion_point(class_scope:tutorial.Person)
))
_sym_db.RegisterMessage(Person) ## 注册
_sym_db.RegisterMessage(Person.PhoneNumber) ## 注册
AddressBook = _reflection.GeneratedProtocolMessageType('AddressBook', (_message.Message,), dict(
DESCRIPTOR = _ADDRESSBOOK,
__module__ = 'addressbook_pb2'
# @@protoc_insertion_point(class_scope:tutorial.AddressBook)
))
_sym_db.RegisterMessage(AddressBook) ## 注册
# @@protoc_insertion_point(module_scope)
- 编写测试代码:test.py。如2.中,_reflection.GeneratedProtocolMessageType用以将每个message最终解释为一个类,通过该类生成支持protobuf序列化的实例。
import gen_protos.addressbook_pb2 as addressbook_pb2
person = addressbook_pb2.Person()
person.id = 1234
person.name = "John Doe"
person.email = "jdoe@example.com"
phone = person.phones.add()
phone.number = "555-4321"
phone.type = addressbook_pb2.Person.HOME
print(f"person:\n{person}")
print(f"phone:\n{phone}")
try:
person.no_such_field = 1 # raises AttributeError
except AttributeError:
print(f"AttributeError")
try:
person.id = "1234" # raises TypeError
except TypeError:
print(f"TypeError")
注意,这些赋值不仅仅是向通用Python对象添加任意的新字段。如果您试图分配一个没有在.proto文件中定义的字段,则会引发AttributeError。如果将字段分配给错误类型的值,则会引发TypeError。此外,在设置字段之前读取字段的值将返回默认值。
枚举
枚举被元类展开为一组带整数值的符号常量。例如,常量addressbook_pb2.Person.PhoneType.WORK的值是2。
Standard Message Methods
每个消息类还包含许多其他方法,让您检查或操作整个消息,包括:
- IsInitialized(): 检查是否所有必需的字段都已设置。
- __str__(): 返回人类可读的消息表示形式,对调试特别有用。(通常作为str(消息)或print消息调用。)
- CopyFrom(other_msg): 用给定消息的值覆盖消息。
- Clear(): 将所有元素清除回空状态。
这些方法实现Message接口。有关更多信息,请参阅Message的完整API文档。
Parsing and Serialization
最后,每个协议缓冲区类都有使用协议缓冲区二进制格式写入和读取所选类型的消息的方法。这些包括:
- SerializeToString(): 序列化消息并将其作为字符串返回。注意,字节是二进制的,不是文本的;我们只使用STR类型作为方便的容器。
- ParseFromString(data):解析来自给定字符串的消息。
这些只是为解析和序列化提供的两个选项。同样,请参阅Message API参考以获得完整的列表。Protocol Buffers and Object Oriented Design2:
Writing A Message
现在让我们试着使用协议缓冲区类。您希望地址簿应用程序能够做的第一件事是向地址簿文件写入个人详细信息。为此,您需要创建并填充(populate instances)协议缓冲区类的实例,然后将它们写入输出流。
下面是一个程序,它从文件中读取一个AddressBook,根据用户输入向其中添加一个新的Person,并再次将新的AddressBook写回文件。直接调用或引用协议编译器生成的代码的部分突出显示。
#! /usr/bin/python3
import gen_protos.addressbook_pb2 as addressbook_pb2
import sys
# This function fills in a Person message based on user input.
def PromptForAddress(person):
person.id = int(input("Enter person ID number: "))
person.name = input("Enter name: ")
email = input("Enter email address (blank for none): ")
if email != "":
person.email = email
while True:
number = input("Enter a phone number (or leave blank to finish): ")
if number == "":
break
phone_number = person.phones.add()
phone_number.number = number
type = input("Is this a mobile, home, or work phone? ")
if type == "mobile":
phone_number.type = addressbook_pb2.Person.PhoneType.MOBILE
elif type == "home":
phone_number.type = addressbook_pb2.Person.PhoneType.HOME
elif type == "work":
phone_number.type = addressbook_pb2.Person.PhoneType.WORK
else:
print("Unknown phone type; leaving as default value.")
# Main procedure: Reads the entire address book from a file,
# adds one person based on user input, then writes it back out to the same
# file.
if len(sys.argv) != 2:
print("Usage:", sys.argv[0], "ADDRESS_BOOK_FILE")
sys.exit(-1)
address_book = addressbook_pb2.AddressBook()
# Read the existing address book.
try:
f = open(sys.argv[1], "rb")
address_book.ParseFromString(f.read())
f.close()
except IOError:
print(sys.argv[1] + ": Could not open file. Creating a new one.")
# Add an address.
PromptForAddress(address_book.people.add())
# Write the new address book back to disk.
f = open(sys.argv[1], "wb")
f.write(address_book.SerializeToString())
f.close()
Reading A Message
当然,如果不能从地址簿中获取任何信息,地址簿就没有多大用处了!这个示例读取由上面的示例创建的文件,并打印其中的所有信息。
#! /usr/bin/python3
import gen_protos.addressbook_pb2 as addressbook_pb2
import sys
# Iterates though all people in the AddressBook and prints info about them.
def ListPeople(address_book):
for person in address_book.people:
print("Person ID:", person.id)
print(" Name:", person.name)
if person.HasField('email'):
print(" E-mail address:", person.email)
for phone_number in person.phones:
if phone_number.type == addressbook_pb2.Person.PhoneType.MOBILE:
print(" Mobile phone #: "),
elif phone_number.type == addressbook_pb2.Person.PhoneType.HOME:
print(" Home phone #: "),
elif phone_number.type == addressbook_pb2.Person.PhoneType.WORK:
print(" Work phone #: "),
print(phone_number.number)
# Main procedure: Reads the entire address book from a file and prints all
# the information inside.
if len(sys.argv) != 2:
print("Usage:", sys.argv[0], "ADDRESS_BOOK_FILE")
sys.exit(-1)
address_book = addressbook_pb2.AddressBook()
# Read the existing address book.
f = open(sys.argv[1], "rb")
address_book.ParseFromString(f.read())
f.close()
ListPeople(address_book)
读写message的Demo
Extending a Protocol Buffer
在您发布使用您的协议缓冲区的代码之后,您迟早会想要“改进”协议缓冲区的定义。如果您希望新的缓冲区向后兼容,旧的缓冲区向前兼容——几乎可以肯定您确实希望这样——那么您需要遵循一些规则。在新版本的协议缓冲区中:
- 您不能更改任何现有字段的标记号[tag numbers]。
- 您不能添加或删除任何必需字段[required fields]。
- 您可以删除可选或重复字段[optional or repeated fields]。
- 您可以添加新的可选或重复字段,但必须使用新的标记号(也就是说,在此协议缓冲区中从未使用过的标记号,甚至被删除的字段也没有使用过)。
(这些规则也有一些例外,但很少使用)
如果遵循这些规则,旧代码将愉快地读取新消息,并简单地忽略任何新字段。对于旧的代码,被删除的可选字段将只具有它们的默认值,而被删除的重复字段将为空。新代码还将透明地读取旧消息。然而,请记住,新的可选字段将不会出现在旧消息中,因此您需要显式检查它们是否用has_设置,或者在.proto文件中提供一个合理的默认值,在标记号后面加上[default = value]。如果没有为可选元素指定默认值,则使用特定于类型的默认值:对于字符串,默认值是空字符串。对于布尔值,默认值为false。对于数字类型,默认值为0。还要注意的是,如果你添加了一个新的重复字段,你的新代码将不能告诉它是空的(新代码)还是根本没有设置(旧代码),因为它没有has_标志。
Advanced Usage【高级用法】
协议缓冲区的用途超出了简单的访问器【accessors】和序列化【serialization】。一定要浏览Python API引用,看看还可以用它们做什么。
协议消息类提供的一个关键特性是反射。您可以遍历消息【iterate over】的字段并操作它们的值,而无需针对任何特定的消息类型编写代码。使用反射的一种非常有用的方法是将协议消息与其他编码(如XML或JSON)进行转换。反射的更高级用途可能是查找相同类型的两个消息之间的差异,或者开发一种“协议消息的正则表达式”,可以在其中编写与特定消息内容匹配的表达式。如果您发挥您的想象力,就有可能将协议缓冲区应用到比您最初预期的更广泛的问题中!
反射作为Message 接口的一部分提供。
您应该非常小心地将字段标记为必需的。如果在某个时刻您希望停止写入或发送一个必填项,那么将该字段更改为可选字段将会有问题——旧的阅读器将认为没有该字段的消息是不完整的,可能会无意中拒绝或丢弃它们。您应该考虑为缓冲区编写特定于应用程序的自定义验证例程。在谷歌中,必填字段强烈不受欢迎;在proto2语法中定义的大多数消息只使用可选的和重复的。(Proto3根本不支持必填字段。) ↩︎
协议缓冲区类基本上是数据持有者(像C中的结构),不提供额外的功能;在对象模型中,它们不能成为优秀的一等公民。如果希望向生成的类添加更丰富的行为,最好的方法是将生成的协议缓冲区类封装在特定于应用程序的类中。如果您无法控制.proto文件的设计(例如,如果您重用来自另一个项目的协议缓冲区),那么封装协议缓冲区也是一个好主意。在这种情况下,您可以使用包装器类来创建一个更适合您的应用程序的独特环境的接口:隐藏一些数据和方法,公开方便的函数,等等。您永远不应该通过继承生成的类向它们添加行为。这将破坏内部机制,并不是良好的面向对象实践。 ↩︎