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。


沒有留言:

張貼留言

2023 Promox on Morefine N6000 16GB 512GB

2023 Promox on Morefine N6000 16GB 512GB Software Etcher 100MB (not but can be rufus-4.3.exe 1.4MB) Proxmox VE 7.4 ISO Installer (1st ISO re...