更多优质内容
请关注公众号

PB协议(三)Protobuf的Python开发教程【翻译自Pb官网】-张柏沛IT博客

正文内容

PB协议(三)Protobuf的Python开发教程【翻译自Pb官网】

栏目:其他内容 系列:PB协议 发布时间:2022-10-29 11:40 浏览量:2182

本文翻译自PB官网的python开发文档,本来想找找看有没有其他博客有人翻译过,但没有找到,只好自己做了。这里附上官网的英文原文链接:

Protocol Buffer Basics: Python (需要翻墙 !-_-)


PB协议(protocol buffer,中文名叫“协议缓冲区”),本教程提供了 Python 使用PB协议的基本介绍。通过创建一个简单的示例应用程序,向您展示如何在文件中定义message消息格式、使用PB编译器、使用 Python 提供的Pb协议 API 来写入和读取message。


如果还不了解Pb协议的朋友可以参考:

PB协议(一)什么是Pb协议(Protobuf),Pb协议如何使用,PB协议的数据类型

此外本文只提供基本python使用PB的示例,如需查看更多python的PB API,可以参考:

Protocol Buffers Python API Reference


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


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

1、使用 Python 的 pickling库进行序列化和反序列化。这是默认方法,因为pickling库是内置于Python的,但它不能很好地处理schema evolution。而且如果您需要与用 C++ 或 Java 编写的应用程序进行数据通信,也不能很好地工作,毕竟C++和Java可没有pickling库,python用pickling库序列化的数据,C++和Java无法按相同的方式反序列化。

2、您可以自定义一种简单的编码解码方式。但缺点是增加了你的开发成本,而且这种编解码方式并不通用,并且你还不能保证它的效率。

3、将数据序列化为 XML。这种方法可能非常有吸引力,因为 XML(某种程度)是人类可读的,并且有许多语言的绑定库。如果您想与其他应用程序/项目共享数据,这可能是一个不错的选择。然而,众所周知,XML 会占用很大的空间,对它进行编码/解码会对应用程序造成巨大的性能损失。

综上,您可以使用PB协议来代替这些选项。协议缓冲区(即PB协议)是解决这个问题的灵活、高效、自动化的解决方案。使用协议缓冲区,您可以编写.proto要存储的数据结构的描述。

protocol buffer 编译器会根据你在.proto文件定义的数据结构创建了一个 python 类,该类以高效的二进制格式实现 protocol buffer 数据的自动编码和解析。生成的类会为你定义的字段提供 getter 和 setter方法,让你可以从二进制数据中读写你定义的字段。

更重要的是,PB协议可以支持扩展字段,也就是说,下次你想往一个接口多增加一个字段,可以直接在proto文件中添加,然后重新编译,编译后的python代码仍然可以兼容旧的数据格式。


一、在proto文件中定义你的数据格式

.proto文件中的定义很简单:为要序列化的每个数据结构添加一个message,然后为message中的每个字段指定名称和类型。

这是定义您的pb文件:addressbook.proto(官网这里使用proto2协议来写的,但推荐大家用最新的proto3)

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;
}


该.proto文件以package(包)声明开头,这有助于防止不同项目之间的命名冲突。

message 是包含一组类型字段的聚合。简单的标量字段类型包括bool、int32、float、double和string。您还可以通过使用其他message类型作为你自定义的字段类型来为消息添加进一步的结构——在上面的示例中,Person这个message包含PhoneNumber这个message,而AddressBook包含Person。

您甚至可以定义嵌套在其他消息中的消息类型——如您所见,PhoneNumber类型是在Person内部定义的。如果您希望其中一个字段的值是多个值中的某一个,您还可以定义enum类型 ,例如在上面的例子中您要指定电话号码可以是以下电话类型之一:MOBILE, HOME, 或WORK。

每个元素上的“= 1”、“= 2”标记标识该字段在二进制编码中使用的唯一“标签号”。标签编号 1-15 比更高的编号需要少一个字节来编码,因此作为一种优化,您可以决定将这些标签用于常用的字段,而将标签 16 和更高的标签用于不太常用的字段。重复字段(repeated类型)表示该字段可以包含多个值,就相当于是数组。


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

  • optional: 该字段可以设置也可以不设置。如果未设置字段值,则使用默认值。

对于简单类型,您可以指定自己的默认值,就像我们在示例中为电话号码type所做的那样。否则,使用系统默认值:数字类型为零,字符串为空字符串,布尔值为 false。对于嵌入式消息,默认值始终是消息的“默认实例”或“原型”,没有设置任何字段。

  • repeated:该字段可以重复任意次数(包括零次)。重复值的顺序将保存在协议缓冲区中。将重复字段视为动态大小的数组。
  • required:必须提供该字段的值,否则该消息将被视为“未初始化”。序列化未初始化的消息将引发异常。解析未初始化的消息将失败。除此之外,必填字段的行为与可选字段完全相同。


PS:Proto3 不支持 required 字段。


二、编译你定义好的pb文件

现在你定义好了.proto文件,接下来需要做的是生成您需要读取和写入AddressBook(以及Person和PhoneNumber)这个数据结构的Python类。为此,您需要用protoc命令编译.proto文件:

protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto

假设你要编译addressbook.proto这个pb文件,那么$SRC_DIR就是你的Pb文件所在的目录,$DST_DIR是编译生成的python类所在的目录。

proto编译器可以在上面我提供的链接中下载,如果因为无法翻墙而访问不了,可以在网上找一找,资源总是有的。


三、在你的程序中用python提供的PB API访问数据

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

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 方法,并将它们添加到相关类中。

这么做可能是为了防止我们修改这些类,毕竟Pb编译后生成的用来访问pb数据的代码是不允许我们修改的。


所有这一切的最终效果是您可以使用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


请注意,如果你往person对象中设置了一个proto文件中不存在的字段,那么python会报一个AttributeError 错误,如果你设置一个错误类型的字段,则会报一个TypeError 错误。如果你没有设置某个字段,就读取这个字段,你会读到这个字段的默认值。

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

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

最后,官网提醒请不要尝试用自定义的类继承这些编译生成的Pb类,这将破坏Pb类的内部机制。


枚举

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

通用的message方法

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

  • IsInitialized(): 检查是否所有必填字段都已设置。
  • __str__():返回我们可读的message数据格式,对于调试特别有用(str(message) or print message)。
  • CopyFrom(other_msg): 用给定的message值覆盖原message。
  • Clear(): 将message的所有字段值清除回空状态。

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


解析和序列化

最后,每个PB类都提供了序列化和反序列化数据的方法。

  • SerializeToString(): 序列化消息并将其作为字符串返回。请注意,返回值的类型是二进制,而不是文本,需要使用.decode方法解析成真正的字符串类型;
  • ParseFromString(data): 从给定的字符串解析消息。

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


在python中编写一个message

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

#! /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()


从一个message中读取

#! /usr/bin/python

import 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中添加新字段)

在你发布你的协议缓冲区的代码之后,迟早你肯定会想要扩展协议缓冲区的定义。如果您希望您的新的Pb数据格式向后兼容,旧的Pb数据格式向前兼容,那么您需要遵循以下规则。在新版本的协议缓冲区中:

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

如果您遵循这些规则,旧代码将轻松的阅读新格式的消息并忽略新格式中的新字段。对于旧代码,已删除的可选字段将仅被赋予其默认值,而删除的重复字段将为空。新代码也将无error的读取旧消息。但是请记住,旧消息中不会出现新的可选字段。


五、高级用法

协议缓冲区的用途远不止简单的调用访问器(getter)和序列化(SerializeToString)。一定要看看 Python API 参考,看看你还能用它们做什么。

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

反射作为 Message接口 的一部分功能提供。




更多内容请关注微信公众号
zbpblog微信公众号

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > PB协议(三)Protobuf的Python开发教程【翻译自Pb官网】

热门推荐
推荐新闻