计算机基础|Protocol Buffer 基础(Python 版)

Protocol Buffer 基础(Python 版) 翻译自:https://developers.google.com/protocol-buffers/docs/pythontutorial
需要使用 protocol buffer 主要分为以下三步:

  • 通过 message 格式定义 .proto 文件
  • 使用 protocol buffer 编译器生成 .py 文件(其他语言类似)
  • 使用 Python protocol buffer API 读写 messages
本文只是基础介绍详细信息可参考:
  • 详细文档:https://developers.google.com/protocol-buffers/docs/overview
  • 版本下载:https://developers.google.com/protocol-buffers/docs/downloads
protocol buffer 的优势
  • Python 内置 pickling:不能很好的应对 schema 的演进,而且不能很好的与 C++ 或者 Java 程序进行数据共享
  • 可以自定义一种转换为字符串的方式。虽然其需要编写编码和解析的代码,并且解析需要一定的时间成本。但因为其简单灵活特别适合处理非常简单的数据。
  • XML:XML 由于易读性并且兼容多种语言而被使用。不过其需要占用相当大的存储空间,同时编解码也需要付出大量的时间成本,最后遍历 XML DOM 树相比于遍历某个类也要复杂的多。
定义 Protocol 格式
这里以一个地址簿的例子做说明。其中地址簿中包含多个联系人,每个联系人将包含一个姓名,ID,电子邮箱和联系电话。这样一个地址簿的 .proto 文件定义如下。这里的例子是以 proto2 为基础,最新的版本为 proto3。
// addressbook.proto syntax = "proto2"; package tutorial; message Person { required string name = 1; required int32 id = 2; optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; }message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; }repeated PhoneNumber phones = 4; }message AddressBook { repeated Person people = 1; }

.proto 文件首先指定包名,其帮组解决不同项目中可能存在的命名冲突问题。不过由于 Python 通过文件路径来管理包名。因此这部分内容只在非 Python 语言中起作用。
接下来就是 message 的定义。message 提供了很多标准的简单数据类型,比如 bool, int32, float, double, string。当然也可以定义一下复杂的数据结构,比如上述程序中,Person 中就包括了 PhoneNumber;同时 AddressBook 包含了 Person。也可以通过枚举预定义变量的取值。
而变量之后的 ‘=1’, '=2' 给出了每个变量唯一的标示。同时,由于 1 - 15 使用的编码大小比 16 以上的少一个字节,因此通常将 1 - 15 留给 required 和 repeated 域。同时,由于 repeated 域中的每一个元素都需要进行标示的重编码,因此这一优化十分适合 repeated 域。
同时,上面的例子中可以发现每一个域都需要一个如下的修饰符:
  • required:这个域必须被赋值,如果未被赋值,那么如果序列化一个这样的 message 将返回一个异常,而如果解析一个未初始化的 message 将会失败。除此之外, required 与 optional 并没有实质的差别。
  • optional:这个域的赋值可有可无。如果没有被赋值,那么默认赋值将被使用。对于简单类,我们可以自己指定,比如上例中的 type。同时每个简单类都提供了一个系统默认的初始值,比如数值类型(0),字符串(空字符串),逻辑变量(false)。对于嵌入式 message,默认值始终是 message 的默认实例或者原型,其中并没有设置其域。如果访问没有显式赋值的域,将总是得到其默认值。
  • repeated:这个可以重复任意次(包括 0)。重复值的顺序将被存储在 protocol buffer 中。repeated 可以被理解为动态数组。
这里必须提醒一点,required 是永久的:因此,在定义域为 required 的时候必须十分谨慎,因为之后如果需要修改,那么使用旧程序的使用者将认为 message 没有正确赋值而报错。同时 Google 内部也就是否需要保留 required 而进行讨论。好像 protocol buffer 3 已经不再显式指明这些修饰符了。
编译 Protocol Buffers
你可以从以下网站https://developers.google.com/protocol-buffers/docs/downloads 并遵循说明安装编译器。
protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto

参数说明:
  • $SRC_DIR:应用所代码,如果不提供将使用当前路径
  • $DST_DIR:生成的代码希望放到哪。由于本例基于 Python,因此使用 --python_out,其他语言类似。
  • 最后是指向 .proto 文件的路径
这行命令将生成一个 addressbook_pb2.py 文件。
Protocol Buffer API
对于 Java 和 C++,当生成 protocol buffer 代码时,将直接给出访问数据的代码,但对于 Python 并不会给出。addressbook_pb2.py 包含以下内容:
class Person(message.Message): __metaclass__ = reflection.GeneratedProtocolMessageTypeclass PhoneNumber(message.Message): __metaclass__ = reflection.GeneratedProtocolMessageType DESCRIPTOR = _PERSON_PHONENUMBER DESCRIPTOR = _PERSONclass AddressBook(message.Message): __metaclass__ = reflection.GeneratedProtocolMessageType DESCRIPTOR = _ADDRESSBOOK

每一个类中重要的信息是 __metaclass__ = reflection.GeneratedProtocolMessageType。具体 Python metaclasses 如何发挥功效可能超过了本文的范围,但你可以把它理解为创建类时需要使用的模板。在加载时,GeneratedProtocolMessageType metaclasses 使用特定的描述符产生所有你需要的 Python 方法并将它们添加到对应的类中。之后就可以在你的代码中使用完全填充好的类了。
比如本文之前使用过的例子,我们就可以通过如下方式使用 message 中定义的 Person 类。
import 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

上述赋值过程并不是简单的增加类中的成员,如果你视图赋值一个没有的变量将返回 AttributeError,而如果你分配了一个错误的类型,那么将返回 TypeError。如果在赋值之前访问一个变量,将返回其默认值。
person.no_such_field = 1# raises AttributeError person.id = "1234"# raises TypeError

枚举类
枚举类由元类扩展为一组具有整数值的符号常量。比如上述例子中 addressbook_pb2.Person.WORK 有一个值 2.
标准 message 方法
每一个 message 还包含某些函数让你可以检查或者操作整个 message。
  • IsInitialized():检测所有的 required 是否被赋值
  • __str__():返回一个可读的 message 表示,通常用于调试中 str(message)和print(message)
解析与序列化
  • SerializeToString():序列化一个 message 并返回字符串。返回的字节是二值的并不是文本,只能使用 str 类型进行存储
  • ParseFromString(data):从给定的字符串中解析 message
其他的解析和序列化函数可以参考:https://developers.google.com/protocol-buffers/docs/reference/python/google.protobuf.message.Message-class
需要注意的是基于 O-O 设计的:Protocol Buffer 本质上是 dumb 的数据存储器,这点有点类似于 C 语言的结构体,这表明其并不能很好的兼容对象模型。如果你想要增加更丰富的行为到一个扩展类中,最好的方式是在应用类中封装扩展的 protocol buffer 类。如果你是复用另一个项目中的 protocol buffer 而无法控制 .proto 文件的设计,那么封装也是一个很好的方式。通过封装,可以更好地适应具体应用的特定环境,比如隐藏某些数据和方法,开放某些方便的函数。但你绝对不要通过继承的方式来扩展类。这将破坏内部的结构并不是一个好的面向对象的方式。
message 读写
#! /usr/bin/pythonimport addressbook_pb2 import sys# This function fills in a Person message based on user input. def PromptForAddress(person): person.id = int(raw_input("Enter person ID number: ")) person.name = raw_input("Enter name: ")email = raw_input("Enter email address (blank for none): ") if email != "": person.email = emailwhile True: number = raw_input("Enter a phone number (or leave blank to finish): ") if number == "": breakphone_number = person.phones.add() phone_number.number = numbertype = raw_input("Is this a mobile, home, or work phone? ") if type == "mobile": phone_number.type = addressbook_pb2.Person.MOBILE elif type == "home": phone_number.type = addressbook_pb2.Person.HOME elif type == "work": phone_number.type = addressbook_pb2.Person.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()

#! /usr/bin/pythonimport 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.emailfor phone_number in person.phones: if phone_number.type == addressbook_pb2.Person.MOBILE: print "Mobile phone #: ", elif phone_number.type == addressbook_pb2.Person.HOME: print "Home phone #: ", elif phone_number.type == addressbook_pb2.Person.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)

扩展现有的 protocol buffer
在你发布你的 protocol buffer 代码之后,不可避免的将会出现需要升级代码的情况,此时如果你需要升级后的代码在之前的代码中仍然可以正常运行(向后兼容),那么升级代码必须满足以下要求:
  • 一定不能改变任何现有域的标示数字(1,2,3,...)
  • 一定不能增加或删除任何的 required 域
  • 可以删除 optional 或者 repeated 域
  • 可以增加新的 optional 或者 repeated 域,但必须保证使用新的标示数字。新的标示数字必须从未被使用,即使被删除的域使用过的标示数字也不行
  • 还有一个些其他的规则,但都不怎么会遇到,如果需要可以参考 https://developers.google.com/protocol-buffers/docs/proto#updating
如此旧代码就可以正常的读取新 meassge 并简单忽略任何新的域。同时就代码对于已经删除的 optional 域将直接使用默认值,而 repeated 域将被置为空。而新代码也将直接读取旧 message。然而必须注意新的 optional 域在旧代码中并不会被提供,因此你必须显式地访问标示位 has_ 来判断其是否被赋值或者通过在标示数字之后使用 [default = value] 来指定默认值。如果默认值没有被指定,那么系统默认值将被使用。同时,对于 repeated 域,由于其没有 has_ 标示位,因此无法获知其是因为新代码没有赋值还是因为旧代码根本就没有设置造成为空。
高阶使用
可以通过如下网站 https://developers.google.com/protocol-buffers/docs/reference/python/ 获取更丰富的使用说明。
其中一个重要的特性就是反射(reflection)。你可以迭代 message 中的域并修改他们的值而不必针对任何特定的 message 类型而修改你的代码。一个很有用的功能是使用反射来实现 protocol buffer 和其他编码格式,比如 XML 或者 JSON 之间的相互转换。而一个高阶的功能是反射可用来找出相同 message 类型中的差异,或者开发出一系列 protocol message 的常规表示,在其中你可以编写匹配特性 message 内容的表达式。同时,你可以发挥自己的想象力从而使用 protocol buffer 来解决更多你可能遇到的问题。
【计算机基础|Protocol Buffer 基础(Python 版)】关于反射的详细介绍可以参考:https://developers.google.com/protocol-buffers/docs/reference/python/google.protobuf.message.Message-class

    推荐阅读