协议缓冲区基础:Python

本教程提供了使用协议缓冲区的基本Python程序员介绍。通过创建一个简单的示例应用程序,它向您展示了如何

  • .proto文件中定义消息格式
  • 使用协议缓冲区编译器。
  • 使用Python协议缓冲区API写入和读取消息。

这不是在Python中使用协议缓冲区的全面指南。有关更多详细的参考信息,请参阅《协议缓冲区语言指南》,《Python API参考》,《Python生成的代码指南》和《编码参考》

为什么要使用协议缓冲区?

我们将使用的示例是一个非常简单的“地址簿”应用程序,它可以在文件中读取和写入人们的联系方式。通讯录中的每个人都有一个姓名,一个ID,一个电子邮件地址和一个联系电话。

您如何像这样序列化和检索结构化数据?有几种方法可以解决此问题:

  • 使用Python酸洗。这是默认方法,因为它已内置在该语言中,但是它不能很好地处理模式演变,如果需要与用C ++或Java编写的应用程序共享数据,也不能很好地工作。
  • 您可以发明一种将数据项编码为单个字符串的临时方法,例如将4个整数编码为“ 12:3:-23:67”。尽管确实需要编写一次性编码和解析代码,但是这是一种简单且灵活的方法,而且解析带来的运行时成本很小。这对于编码非常简单的数据最有效。
  • 将数据序列化为XML。由于XML是人类(一种)可读的,并且存在用于多种语言的绑定库,因此这种方法可能非常有吸引力。如果要与其他应用程序/项目共享数据,这可能是一个不错的选择。但是,众所周知,XML占用大量空间,对它进行编码/解码会给应用程序带来巨大的性能损失。同样,导航XML DOM树比通常导航类中的简单字段要复杂得多。

协议缓冲区是解决此问题的灵活,高效,自动化的解决方案。使用协议缓冲区,您可以编写.proto要存储的数据结构描述。由此,协议缓冲区编译器创建了一个类,该类以有效的二进制格式实现协议缓冲区数据的自动编码和解析。生成的类为构成协议缓冲区的字段提供了获取器和设置器,并以协议为单位来处理读写协议缓冲区的细节。重要的是,协议缓冲区格式支持以某种方式扩展格式的思想,以使代码仍可以读取以旧格式编码的数据。

在哪里可以找到示例代码

示例代码包含在源代码包中的“ 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。让我们浏览文件的每个部分,看看它的作用。

.proto文件以程序包声明开头,这有助于防止不同项目之间的命名冲突。在Python中,包通常由目录结构决定,因此package您在.proto文件中定义的包不会对生成的代码产生影响。但是,您仍然应该声明一个以避免在协议缓冲区名称空间以及非Python语言中的名称冲突。

接下来,您将拥有消息定义。消息只是包含一组类型字段的汇总。许多标准的简单数据类型都可以作为字段类型,包括boolint32floatdouble,和string您还可以通过使用其他消息类型作为字段类型来为消息添加更多结构-在上述示例中,Person消息包含PhoneNumber消息,而AddressBook消息包含Person消息。您甚至可以定义嵌套在其他消息内的消息类型-如您所见,该PhoneNumber类型在内部定义Personenum如果您希望某个字段具有一个预定义的值列表之一,也可以定义类型-在这里您要指定电话号码可以是MOBILEHOMEWORK

每个元素上的“ = 1”,“ = 2”标记标识该字段在二进制编码中使用的唯一“标记”。标签编号1至15与较高的编号相比,编码所需的字节减少了一个字节,因此,为了进行优化,您可以决定将这些标签用于常用或重复的元素,而将标签16和更高的标签用于较少使用的可选元素。重复字段中的每个元素都需要重新编码标签号,因此重复字段是此优化的最佳候选者。

每个字段都必须使用以下修饰符之一进行注释:

  • optional:可能会或可能不会设置该字段。如果未设置可选字段值,则使用默认值。对于简单类型,您可以指定自己的默认值,就像type在示例中为电话号码所做的那样否则,将使用系统默认值:数字类型为零,字符串为空字符串,布尔值为false。对于嵌入式消息,默认值始终是消息的“默认实例”或“原型”,没有设置任何字段。调用访问器以获取未显式设置的可选(或必填)字段的值始终会返回该字段的默认值。
  • repeated:该字段可以重复任意次(包括零次)。重复值的顺序将保留在协议缓冲区中。将重复字段视为动态大小的数组。
  • required:必须提供该字段的值,否则该消息将被视为“未初始化”。序列化未初始化的消息将引发异常。解析未初始化的消息将失败。除此之外,必填字段的行为与可选字段完全相同。

永远是必需的 您将字段标记为时应非常小心required如果您希望停止写入或发送必填字段,则将字段更改为可选字段会很麻烦–老读者会认为没有该字段的邮件是不完整的,可能会无意中拒绝或丢弃它们。您应该考虑为缓冲区编写特定于应用程序的自定义验证例程。在Google内部,required强烈反对使用字段;proto2语法中定义的大多数消息使用optional并且repeated(Proto3根本不支持required字段。)

.proto可以在“协议缓冲区语言指南”中找到有关编写文件的完整指南,包括所有可能的字段类型但是,不要去寻找类似于类继承的工具–协议缓冲区不能做到这一点。

编译协议缓冲区

既然有了.proto,接下来需要做的就是生成读取和写入AddressBook(以及因此PersonPhoneNumber)消息所需的类。要做到这一点,你需要运行协议缓冲编译器protoc对您.proto

  1. 如果尚未安装编译器,请下载软件包并按照README中的说明进行操作。
  2. 现在运行编译器,指定源目录(应用程序的源代码所在的位置;如果不提供值,则使用当前目录),目标目录(您希望生成的代码进入的位置;通常与相同$SRC_DIR)以及通向您的道路.proto在这种情况下,您...:
     
    protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto
    

      

    因为您需要Python类,所以可以使用该--python_out选项–其他受支持的语言也提供了类似的选项。

这将addressbook_pb2.py在您指定的目标目录中生成

协议缓冲区API

与生成Java和C ++协议缓冲区代码时不同,Python协议缓冲区编译器不会直接为您生成数据访问代码。相反(如您所见,您会看到addressbook_pb2.py它)为所有消息,枚举和字段以及一些神秘的空类生成特殊的描述符,每种消息类型一个:

class Person(message.Message):
  __metaclass__ = reflection.GeneratedProtocolMessageType

  class PhoneNumber(message.Message):
    __metaclass__ = reflection.GeneratedProtocolMessageType
    DESCRIPTOR = _PERSON_PHONENUMBER
  DESCRIPTOR = _PERSON

class AddressBook(message.Message):
  __metaclass__ = reflection.GeneratedProtocolMessageType
  DESCRIPTOR = _ADDRESSBOOK

  

每个班级的重要一行是__metaclass__ = reflection.GeneratedProtocolMessageType尽管Python元类如何工作的详细信息超出了本教程的范围,但是您可以将它们视为创建类的模板。在加载时,GeneratedProtocolMessageType元类使用指定的描述符创建每种消息类型都需要使用的所有Python方法,并将它们添加到相关的类中。然后,您可以在代码中使用完全填充的类。

所有这些的最终结果是,您可以像使用Person类一样将Message基类的每个字段定义为常规字段。例如,您可以编写:

 
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

  

请注意,这些分配不只是向通用Python对象添加任意新字段。如果您尝试分配.proto文件中未定义的字段,AttributeError则会引发。如果将字段分配给错误类型的值,TypeError则会引发a。同样,在设置字段之前读取它的值将返回默认值。

 
person.no_such_field = 1  # raises AttributeError
person.id = "1234"        # raises TypeError

 

有关协议编译器为任何特定字段定义生成的成员的确切信息,请参见Python生成的代码参考

枚举

元类将枚举扩展为具有整数值的一组符号常量。因此,例如,常数addressbook_pb2.Person.PhoneType.WORK的值为2。

标准消息方法

每个消息类还包含许多其他方法,可用于检查或操作整个消息,包括:

  • IsInitialized():检查是否已设置所有必填字段。
  • __str__():返回消息的可读形式,对于调试特别有用。(通常以str(message)调用print message。)
  • CopyFrom(other_msg):使用给定消息的值覆盖消息。
  • Clear():将所有元素清除为空状态。

这些方法实现Message接口。有关更多信息,请参见的完整API文档Message

解析和序列化

最后,每个协议缓冲区类都有使用协议缓冲区二进制格式写入和读取所选类型的消息的方法这些包括:

  • SerializeToString():序列化消息并以字符串形式返回。注意字节是二进制的,不是文本;我们仅将str类型用作方便的容器。
  • ParseFromString(data):解析给定字符串中的消息。

这些只是为解析和序列化提供的几个选项。同样,请参阅MessageAPI参考以获取完整列表。

协议缓冲区和面向对象的设计协议缓冲区类基本上是愚蠢的数据持有者(如C中的结构)。他们没有在对象模型中成为优秀的一等公民。如果要向生成的类添加更丰富的行为,最好的方法是将生成的协议缓冲区类包装在特定于应用程序的类中。如果您无法控制.proto文件的设计,则包装协议缓冲区也是一个好主意(例如,如果您要重用另一个项目中的文件)。在这种情况下,您可以使用包装器类来设计一个更适合您的应用程序独特环境的接口:隐藏一些数据和方法,公开便利函数等。您永远不应通过从它们继承来向生成的类添加行为这将破坏内部机制,无论如何都不是一个好的面向对象的实践。

写一条消息

现在,让我们尝试使用协议缓冲区类。您希望地址簿应用程序能够做的第一件事是将个人详细信息写入地址簿文件。为此,您需要创建并填充协议缓冲区类的实例,然后将它们写入输出流。

这是一个程序,它AddressBook从文件中读取一个Person然后根据用户输入向其中添加一个新AddressBook文件,然后再次将新文件回到文件中。突出显示直接调用或引用协议编译器生成的代码的部分。

 
#! /usr/bin/python

import 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 = email

  while True:
    number = raw_input("Enter a phone number (or leave blank to finish): ")
    if number == "":
      break

    phone_number = person.phones.add()
    phone_number.number = number

    type = raw_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()

  

扩展协议缓冲区

在发布使用协议缓冲区的代码后早晚,您无疑会想要“改进”协议缓冲区的定义。如果您希望新的缓冲区向后兼容,而旧的缓冲区向后兼容,而您几乎肯定希望这样做,那么您需要遵循一些规则。在新版本的协议缓冲区中:

  • 不得更改任何现有字段的标签号。
  • 不得添加或删除任何必填字段。
  • 可以删除可选字段或重复字段。
  • 可以添加新的可选或重复字段,但必须使用新的标签号(即,该协议缓冲区中从未使用过的标签号,甚至删除的字段也从未使用过)。

这些规则一些例外,但很少使用。)

如果遵循这些规则,旧代码将愉快地读取新消息,而忽略任何新字段。对于旧代码,已删除的可选字段将仅具有其默认值,而删除的重复字段将为空。新代码还将透明地读取旧消息。但是,请记住,新的可选字段将不会出现在旧消息中,因此您需要明确检查是否已使用设置了这些字段,或者需要使用来has_.proto文件中提供合理的默认值[default = value]标签编号之后。如果未为可选元素指定默认值,则使用特定于类型的默认值:对于字符串,默认值为空字符串。对于布尔值,默认值为false。对于数字类型,默认值为零。还要注意,如果添加了一个新的重复字段,则由于没有has_标记,因此新代码将无法分辨它是空的(由新代码)还是根本没有设置(由旧代码)

高级用法

协议缓冲区的用途不只是简单的访问器和序列化。请务必浏览Python API参考,以了解您还可以使用它们做什么。

协议消息类提供的一项关键功能是反射您可以遍历消息的字段并操纵它们的值,而无需针对任何特定的消息类型编写代码。使用反射的一种非常有用的方法是将协议消息与其他编码(例如XML或JSON)相互转换。反射的一种更高级的用法可能是查找同一类型的两条消息之间的差异,或者开发一种“协议消息的正则表达式”,在其中您可以编写与某些消息内容匹配的表达式。如果您发挥自己的想象力,可以将协议缓冲区应用于比最初预期的范围更广的问题!

反射是Message接口的一部分

 
源:https://developers.google.com/protocol-buffers/docs/pythontutorial

 

 

 

posted @ 2020-12-13 16:03  DaisyLinux  阅读(507)  评论(0编辑  收藏  举报