2016年7月29日 星期五

Protocol Buffer for Python 教学

这篇文章会介绍

一:如何定义proto讯息格式
二:使用protocol buffer编译器
三:视同protocol buffer读写信息

为什么要使用protocol buffer?

这个例子使用一个非常简单的联络薄,允许读写。每笔资料代表一个人的名字,ID,邮箱地址及电话号码。

要如何把这些资料序列化及读取?我们有以下几个方法:

使用Python Pickle。这是Python预设的方法,可是这个方法不能有效处理Schema改动,而且要与C++及Java程序交换资料很难。

发明一个方法把资料变成字串,举例,4个整数可以变成“12:3”-23:67“。虽然这歌方法不能提供一次性编码及解析代码,而且解析需要一点运行成本,可是这个方法很简单有弹性,适合一些很简单的资料。

把资料序列化为XML,这很吸引因为XML是人类可读的,而且很多不同语言都支持。如果你要把资料分享给其他应用,XML是一个很好的选择。可是,XML空间要求巨大,早就臭名远播,而且编码对性能的负面影响很,XML DOM树明显比简单的读取一个class field来的复杂很多。

Protocol Buffer刚好可以解决这个问题。有了Protocol buffer,我们可以把资料结构写进一个proto描述档案,使用protocol buffer编译器从这个proto档产生class档案并且拥有一个高效率二进制而自动产生出来的编码及解析代码。产生出来的class提供getter及setter,处理读写细节。更重要的是,protocol buffer格式支持随着时间的格式延伸,代码可以读取使用就格式编码的资料。

定义你的协议格式

要建立一个联络薄应用,首先要从proto档案开始,首先加为每个资料结构加入一个message,然后为每个属性建立一个名字跟形态。看以下例子。

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 phone = 4;
}

message AddressBook {
  repeated Person person = 1;
}

可以看到,文法跟C++及Java很像似。详细解释如下。

首先,一开始写一个package宣告,用来避免将来有可能出现的名字冲突。在Python里面,packages一般用资料夹结构去决定,所以在这个proto档案里使用package并不会影响产生出来的代码。可是,你还是应该宣告一个package去避免在各个protocol buffer名字空间的名字冲突以及避免Python意外的语言的名字冲突。

然后,进入messsage定义部分。一个message集合了一堆有形态的属性。很多的简单形态都可以使用,包括bool,int32,float,double,string。你也可以使用自定义形态。上面的例子Person就包含了PhoneNumber这个形态。你也可以定义enum形态去限制PhoneNumber的type为MOBILE,HOME,WORK其中之一。

你看到的“=1”,“=2”标记是用来识别独一无二的tag并用于二进制编码里面。tag号码1-15比正常的编码使用省1字节,因此如果要要优化的话我们可以决定把常用或者重复的元素予以1-15的tag,而16以上的tag留给不常见可有可无的元素。每个重复的元素要求把tag号码重新编码,所以重复的属性特别适合使用这个优化方法。

每个属性必须要用一下其中一个方法去描述一下:

required:必须提供一个数值给这个属性,否则这个message会被认为是未初始化。序列化一个未初始化的message会引起一个例外。解析一个未初始化的message会失败。除此以外,required属性跟optional属性一模一样。

optional:这个属性可以设定也可以不。如果没设定的话,预设值会被使用。简单的类型,我们可以指明预设值。举上例,phone number type预设使用HOME。如果没有指明预设值,系统预设值会被使用:数值类型用0,字串类型用空字串,布朗值用否。使用embedded messages的话,预设值永远是message的“default instance“或者”prototype“并且不会设置任何一个属性。如果用accessor向一个为设定的属性取值的话,会返回那个属性的预设值。

repeated:这个属性可能会重复0次或多次,就是可有可无的意思。重复的数值的顺序会被保留。可以把重复属性想象成动态大小的数列。

Required代表永远
使用required的时候要特别小心,如果某天打算停止写入及读取某个required属性,想把它从required改成optional将会产生很大问题。旧的读取者会认为缺少这个属性的message为不完整并且有可能漫无目的地拒绝或者扔掉这些messages。我们应该考虑使用特别为应用建立的检查有效性的代码取而代之。Google某些工程师得出以下结论:使用required斃多于利,他们倾向只使用optional及repeated。可是这个观点并不能一概而论。

把protocol buffer编译
看完proto档案怎么编写,现在可以编译了。首先你要看一大堆文章把protoc编译器安装好。直到有一天你在command line可以顺利跑protoc --version,你便算是安装好编译器了。
复杂点:protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto
简单点:protoc --python_out=. addressbook.proto
然后就会看见产生出来的档案addressbook_pb2.py

Protocol Buffer API
与C++及Java的代码产生不同,Python protocol buffer编译器不会为你直接产生资料提取代码,却会为所有messages,enums,fields及某些神秘的空类产生特别的描述器。
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详细如何工作不再此述,我们可以把它暂时把它想象成一个建立类的模板。在载入的时候,GeneratedProtocolMessageType元类视同特定的描述器为所有我们需要用到的讯息的产生相应的Python方法及相关的类。
你可以使用Person类犹如每个field已经定义在Message一样,例如
import addressbook_pb2
person = addressbook_pb2.Person()
person.id = 1234
person.name = "John Doe"
person.email = "jdoe@example.com"
phone = person.phone.add()
phone.number = "555-4321"
phone.type = addressbook_pb2.Person.HOME
留意以上这些assingments并不是单纯把一些随意的field加到generic Python物件里面的。如果你尝试assign一些还没有在proto档定义的field,系统会抛出AttributeError。如果assign错type,系统会抛出TypeError。读取未设定的field将会返回预设值。
person.no_such_field = 1  # raises AttributeError
person.id = "1234"        # raises TypeError
Enums
addressbook_pb2.Person.WOKR拥有2的值。

Standard Message Methods
每个讯息类别都有一堆方法让你检查及操作这个类的。
IsInitialized():检查是不是所有fields都已经赋值了
__str__():返回human-readable的字串,侦错时特有效,例如str(message) or pirnt(message)
CopyFrom(other_msg):覆盖当前讯息
Clear():清空为空的状态
这几个方法都是implement了class Message的界面。

Parsing and Serialization
每个protocol buffer class都有使用protocol buffer binary format方法去读写。
SerializeToString():讯息变字串。留意那些字节都是binary而不是text。我们仅仅用str type作为一个方便的container
ParseFromString():string变讯息

Protocol Buffers and OO Design
Protocol Buffer class 仅仅是资料类别,不要当成object model。如果想要更丰富的behaviour,最好的方法是wrap掉protocol buffer class成为applicationg-specific class。如果对于proto档案没有控制权的开发者,warpping也是很好的方法,因为我们可以隐藏某些不扼要的资料跟方法,并且暴露有用的方法。记住,永远不要用继承产生出来的class去增加或者改变class的行为,这样会打破内部机制。

Writing A Message
以下例子首先从档案读取AddressBook,添加一个Person,把新的AddressBook写进档案。黄色的是跟protocol compiler产生出来的代码有关的。
#! /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.phone.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.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.person.add())

# Write the new address book back to disk.
f = open(sys.argv[1], "wb")
f.write(address_book.SerializeToString())
f.close()
Extending a Protocol Buffer
往后当我们需要改进protocol buffer定义时候,如果需要让新buffer往前兼容并且让旧buffer往前兼容,有些规矩需要遵守的。
1. 一定不可以更改现有的任何tag number
2. 一定不可以加减任何required fields
3. 可以删除optional或者repeated fields
4. 可以增加新的optional或者repeated fields但是一定要用全新的tag numbers, 全新的意思是那些永远未被使用过的tag numbers,连删除了的fields也不曾使用过的

如果乖乖的遵守以上规条,旧代码会快乐的读取新讯息以及忽略新fields,因为对于旧代码来说,删除了的optional fields会有default values,而删除了的repeated fields会变成empty。新代码会读取就信息。可是,旧讯息一定不会存在任何新的optional fields,所以,要嘛用has_故意检查他们是否已经设定好,要嘛用[default=value]去提供一个合理的预设值。如果optional fields没有提供预设值,系统会给一个:空,否,零。如果新加了repeated field,新代码不能判断到底是新代码设定为空的,还是就代码没有设定,因为repeated field没有 has_ field。


沒有留言:

張貼留言

2007 to 2023 HP and Dell Servers Comparison

  HP Gen5 to Gen11  using ChatGPT HP ProLiant Gen Active Years CPU Socket Popular HP CPUs Cores Base Clock Max RAM Capacity Comparable Dell ...