Protocol Buffer 基础(Python 版)
Protocol Buffer 基礎(chǔ)(Python 版)
翻譯自:https://developers.google.com/protocol-buffers/docs/pythontutorial
需要使用?protocol?buffer 主要分為以下三步:
- 通過?message?格式定義 .proto?文件
- 使用?protocol?buffer?編譯器生成 .py?文件(其他語言類似)
- 使用?Python?protocol?buffer?API?讀寫?messages
本文只是基礎(chǔ)介紹詳細(xì)信息可參考:
- 詳細(xì)文檔:https://developers.google.com/protocol-buffers/docs/overview
- 版本下載:https://developers.google.com/protocol-buffers/docs/downloads
protocol?buffer?的優(yōu)勢
- Python?內(nèi)置?pickling:不能很好的應(yīng)對 schema?的演進(jìn),而且不能很好的與?C++?或者?Java?程序進(jìn)行數(shù)據(jù)共享
- 可以自定義一種轉(zhuǎn)換為字符串的方式。雖然其需要編寫編碼和解析的代碼,并且解析需要一定的時間成本。但因為其簡單靈活特別適合處理非常簡單的數(shù)據(jù)。
- XML:XML?由于易讀性并且兼容多種語言而被使用。不過其需要占用相當(dāng)大的存儲空間,同時編解碼也需要付出大量的時間成本,最后遍歷?XML?DOM?樹相比于遍歷某個類也要復(fù)雜的多。
定義?Protocol?格式
這里以一個地址簿的例子做說明。其中地址簿中包含多個聯(lián)系人,每個聯(lián)系人將包含一個姓名,ID,電子郵箱和聯(lián)系電話。這樣一個地址簿的 .proto?文件定義如下。這里的例子是以?proto2?為基礎(chǔ),最新的版本為?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?通過文件路徑來管理包名。因此這部分內(nèi)容只在非?Python?語言中起作用。
接下來就是 message?的定義。message?提供了很多標(biāo)準(zhǔn)的簡單數(shù)據(jù)類型,比如?bool, int32, float, double, string。當(dāng)然也可以定義一下復(fù)雜的數(shù)據(jù)結(jié)構(gòu),比如上述程序中,Person?中就包括了?PhoneNumber;同時?AddressBook?包含了 Person。也可以通過枚舉預(yù)定義變量的取值。
而變量之后的 ‘=1’, '=2'?給出了每個變量唯一的標(biāo)示。同時,由于 1 - 15?使用的編碼大小比 16?以上的少一個字節(jié),因此通常將 1 - 15?留給?required?和 repeated?域。同時,由于?repeated?域中的每一個元素都需要進(jìn)行標(biāo)示的重編碼,因此這一優(yōu)化十分適合?repeated?域。
同時,上面的例子中可以發(fā)現(xiàn)每一個域都需要一個如下的修飾符:
- required:這個域必須被賦值,如果未被賦值,那么如果序列化一個這樣的?message?將返回一個異常,而如果解析一個未初始化的?message?將會失敗。除此之外,?required?與?optional?并沒有實質(zhì)的差別。
- optional:這個域的賦值可有可無。如果沒有被賦值,那么默認(rèn)賦值將被使用。對于簡單類,我們可以自己指定,比如上例中的?type。同時每個簡單類都提供了一個系統(tǒng)默認(rèn)的初始值,比如數(shù)值類型(0),字符串(空字符串),邏輯變量(false)。對于嵌入式?message,默認(rèn)值始終是?message?的默認(rèn)實例或者原型,其中并沒有設(shè)置其域。如果訪問沒有顯式賦值的域,將總是得到其默認(rèn)值。
- repeated:這個可以重復(fù)任意次(包括 0)。重復(fù)值的順序?qū)⒈淮鎯υ?protocol buffer?中。repeated?可以被理解為動態(tài)數(shù)組。
這里必須提醒一點,required?是永久的:因此,在定義域為?required?的時候必須十分謹(jǐn)慎,因為之后如果需要修改,那么使用舊程序的使用者將認(rèn)為?message?沒有正確賦值而報錯。同時?Google?內(nèi)部也就是否需要保留?required?而進(jìn)行討論。好像?protocol?buffer 3?已經(jīng)不再顯式指明這些修飾符了。
編譯?Protocol?Buffers
你可以從以下網(wǎng)站??https://developers.google.com/protocol-buffers/docs/downloads?并遵循說明安裝編譯器。
protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto參數(shù)說明:
- $SRC_DIR:應(yīng)用所代碼,如果不提供將使用當(dāng)前路徑
- $DST_DIR:生成的代碼希望放到哪。由于本例基于?Python,因此使用 --python_out,其他語言類似。
- 最后是指向 .proto?文件的路徑
這行命令將生成一個?addressbook_pb2.py?文件。
Protocol?Buffer API
對于 Java?和?C++,當(dāng)生成?protocol?buffer?代碼時,將直接給出訪問數(shù)據(jù)的代碼,但對于?Python?并不會給出。addressbook_pb2.py?包含以下內(nèi)容:
class Person(message.Message):__metaclass__ = reflection.GeneratedProtocolMessageTypeclass PhoneNumber(message.Message):__metaclass__ = reflection.GeneratedProtocolMessageTypeDESCRIPTOR = _PERSON_PHONENUMBERDESCRIPTOR = _PERSONclass AddressBook(message.Message):__metaclass__ = reflection.GeneratedProtocolMessageTypeDESCRIPTOR = _ADDRESSBOOK每一個類中重要的信息是?__metaclass__ = reflection.GeneratedProtocolMessageType。具體?Python?metaclasses?如何發(fā)揮功效可能超過了本文的范圍,但你可以把它理解為創(chuàng)建類時需要使用的模板。在加載時,GeneratedProtocolMessageType?metaclasses?使用特定的描述符產(chǎn)生所有你需要的?Python?方法并將它們添加到對應(yīng)的類中。之后就可以在你的代碼中使用完全填充好的類了。
比如本文之前使用過的例子,我們就可以通過如下方式使用?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。如果在賦值之前訪問一個變量,將返回其默認(rèn)值。
person.no_such_field = 1 # raises AttributeError person.id = "1234" # raises TypeError枚舉類
枚舉類由元類擴(kuò)展為一組具有整數(shù)值的符號常量。比如上述例子中?addressbook_pb2.Person.WORK?有一個值 2.
標(biāo)準(zhǔn)?message?方法
每一個?message?還包含某些函數(shù)讓你可以檢查或者操作整個?message。
- IsInitialized():檢測所有的?required?是否被賦值
- __str__():返回一個可讀的?message?表示,通常用于調(diào)試中?str(message)和print(message)
解析與序列化
- SerializeToString():序列化一個?message?并返回字符串。返回的字節(jié)是二值的并不是文本,只能使用?str?類型進(jìn)行存儲
- ParseFromString(data):從給定的字符串中解析?message
其他的解析和序列化函數(shù)可以參考:https://developers.google.com/protocol-buffers/docs/reference/python/google.protobuf.message.Message-class
需要注意的是基于?O-O?設(shè)計的:Protocol?Buffer?本質(zhì)上是?dumb?的數(shù)據(jù)存儲器,這點有點類似于?C?語言的結(jié)構(gòu)體,這表明其并不能很好的兼容對象模型。如果你想要增加更豐富的行為到一個擴(kuò)展類中,最好的方式是在應(yīng)用類中封裝擴(kuò)展的?protocol?buffer?類。如果你是復(fù)用另一個項目中的?protocol?buffer?而無法控制 .proto?文件的設(shè)計,那么封裝也是一個很好的方式。通過封裝,可以更好地適應(yīng)具體應(yīng)用的特定環(huán)境,比如隱藏某些數(shù)據(jù)和方法,開放某些方便的函數(shù)。但你絕對不要通過繼承的方式來擴(kuò)展類。這將破壞內(nèi)部的結(jié)構(gòu)并不是一個好的面向?qū)ο蟮姆绞健?/span>
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.MOBILEelif type == "home":phone_number.type = addressbook_pb2.Person.HOMEelif type == "work":phone_number.type = addressbook_pb2.Person.WORKelse: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.idprint " Name:", person.nameif 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)擴(kuò)展現(xiàn)有的?protocol?buffer
在你發(fā)布你的?protocol?buffer?代碼之后,不可避免的將會出現(xiàn)需要升級代碼的情況,此時如果你需要升級后的代碼在之前的代碼中仍然可以正常運行(向后兼容),那么升級代碼必須滿足以下要求:
- 一定不能改變?nèi)魏维F(xiàn)有域的標(biāo)示數(shù)字(1,2,3,...)
- 一定不能增加或刪除任何的?required?域
- 可以刪除?optional?或者?repeated?域
- 可以增加新的?optional?或者?repeated?域,但必須保證使用新的標(biāo)示數(shù)字。新的標(biāo)示數(shù)字必須從未被使用,即使被刪除的域使用過的標(biāo)示數(shù)字也不行
- 還有一個些其他的規(guī)則,但都不怎么會遇到,如果需要可以參考?https://developers.google.com/protocol-buffers/docs/proto#updating
如此舊代碼就可以正常的讀取新?meassge?并簡單忽略任何新的域。同時就代碼對于已經(jīng)刪除的?optional?域?qū)⒅苯邮褂媚J(rèn)值,而?repeated?域?qū)⒈恢脼榭铡6麓a也將直接讀取舊?message。然而必須注意新的?optional?域在舊代碼中并不會被提供,因此你必須顯式地訪問標(biāo)示位 has_?來判斷其是否被賦值或者通過在標(biāo)示數(shù)字之后使用 [default = value]?來指定默認(rèn)值。如果默認(rèn)值沒有被指定,那么系統(tǒng)默認(rèn)值將被使用。同時,對于?repeated?域,由于其沒有?has_?標(biāo)示位,因此無法獲知其是因為新代碼沒有賦值還是因為舊代碼根本就沒有設(shè)置造成為空。
高階使用
可以通過如下網(wǎng)站?https://developers.google.com/protocol-buffers/docs/reference/python/?獲取更豐富的使用說明。
其中一個重要的特性就是反射(reflection)。你可以迭代?message?中的域并修改他們的值而不必針對任何特定的?message?類型而修改你的代碼。一個很有用的功能是使用反射來實現(xiàn)?protocol?buffer?和其他編碼格式,比如?XML?或者?JSON?之間的相互轉(zhuǎn)換。而一個高階的功能是反射可用來找出相同?message?類型中的差異,或者開發(fā)出一系列?protocol?message?的常規(guī)表示,在其中你可以編寫匹配特性?message?內(nèi)容的表達(dá)式。同時,你可以發(fā)揮自己的想象力從而使用?protocol?buffer?來解決更多你可能遇到的問題。
關(guān)于反射的詳細(xì)介紹可以參考:https://developers.google.com/protocol-buffers/docs/reference/python/google.protobuf.message.Message-class
總結(jié)
以上是生活随笔為你收集整理的Protocol Buffer 基础(Python 版)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Protocol Buffer 简介
- 下一篇: Python3练习题系列(01)