在Python中使用Protocol?Buffers的詳細介紹
實踐環(huán)境
protoc-25.4-win64.zip
下載地址:
https://github.com/protocolbuffers/protobuf/releases
https://github.com/protocolbuffers/protobuf/releases/download/v25.4/protoc-25.4-win64.zip
protobuf 5.27.2
pip install protobuf==5.27.2
Python 3.9.13
問題域
本文將使用的示例是一個非常簡單的“地址簿”應用程序,它可以從文件中讀取和寫入人們的聯系方式。通訊簿中的每個人都有一個姓名、一個ID、一個電子郵件地址和一個聯系電話號碼。
如何序列化和檢索這樣的結構化數據?有幾種方法可以解決這個問題:
使用Python pickle。這是默認方法,因為它內置于語言中,但它不能很好地處理模式演化,如果你需要與用C++或Java編寫的應用程序共享數據,它也不能很好的工作。
你可以發(fā)明一種特殊的方法將數據項編碼為單個字符串,例如將4個整數編碼為“12:3:-23:67”。這是一種簡單而靈活的方法,盡管它確實需要編寫一次性編碼和解析代碼,并且解析帶來的運行時成本很小。這最適合對非常簡單的數據進行編碼。
將數據序列化為XML。這種方法非常有吸引力,因為XML(某種程度上)是人類可讀的,并且有許多語言的綁定庫。如果想與其他應用程序/項目共享數據,這可能是一個不錯的選擇。然而,眾所周知,XML是空間密集型的,對其進行編碼/解碼會給應用程序帶來巨大的性能損失。此外訪問XML DOM樹訪問類中的簡單字段要復雜得多。
可以使用協議緩沖區(qū)(Protocol buffers)替代這些選擇。協議緩沖區(qū)是解決這個問題的靈活、高效、自動化的解決方案。使用協議緩沖區(qū) ,可以編寫希望存儲的數據結構的.proto
描述。協議緩沖區(qū)編譯器將從該文件創(chuàng)建一個類,該類以有效的二進制格式實現協議緩沖區(qū)數據的自動編碼和解析。生成的類為構成協議緩沖區(qū)的字段提供getters
和setters
方法,并處理將協議緩沖區(qū)作為一個單元進行讀寫的細節(jié)。重要的是,協議緩沖區(qū)格式支持隨著時間的推移擴展格式的想法,這樣代碼仍然可以讀取用舊格式編碼的數據。
定義協議格式(編寫proto文件)
要創(chuàng)建地址簿應用程序,需要從.proto
文件開始。.proto
文件中的定義很簡單:為要序列化的每個數據結構添加一個消息(message
),然后為消息中的每個字段指定名稱和類型。
示例:addressbook.proto
syntax = "proto2"; // proto2指定proto buffer的版本 package tutorial; message Person { optional string name = 1; optional int32 id = 2; optional string email = 3; enum PhoneType { PHONE_TYPE_UNSPECIFIED = 0; PHONE_TYPE_MOBILE = 1; PHONE_TYPE_HOME = 2; PHONE_TYPE_WORK = 3; } message PhoneNumber { optional string number = 1; optional PhoneType type = 2 [default = PHONE_TYPE_HOME]; } repeated PhoneNumber phones = 4; // phones字段是一個重復字段,可以包含多個電話號碼。 } message AddressBook { repeated Person people = 1; }
說明:
以上這個.proto
文件以package
聲明開始,這有助于防止不同項目之間的命名沖突。在Python中,包通常由目錄結構決定,因此在.proto
文件定義的package
對生成的代碼沒有影響。但是,仍然應該聲明一個package
,以避免在協議緩沖區(qū)名稱空間以及非Python語言中的名稱沖突。
接下來,是消息定義。消息只是包含一組類型字段的集合。許多標準的簡單數據類型可以作為字段類型使用,包括bool
、int32
、float
、double
和string
。還可以通過使用其他消息類型作為字段類型來為消息添加更多的結構 - 在上面的示例中,Person
消息包含PhoneNumber
消息,而AddressBook
消息包含Person
消息。甚至可以定義嵌套在其他消息中的消息類型-如上,PhoneNumber
類型定義在Person
中。如果希望其中一個字段具有預定義的值列表之一,也可以定義枚舉類型 - 在這里希望指定電話號碼可以是以下電話類型之一:
PHONE_TYPE_MOBILE
PHONE_TYPE_HOME
PHONE_TYPE_WORK
每個元素上的“=1”、“=2”標記標識該字段在二進制編碼中使用的唯一“標記”,這確保了在序列化和反序列化過程中,每個字段可以被正確地識別和處理。這些數字標簽在編譯時被轉換為命名空間和類型簽名,從而保證了字段的唯一性。使用1-15的標記編號比使用更高的數字要少一個字節(jié)編碼,因此作為優(yōu)化,可以決定將這些標簽用于常用或重復的元素,將16及更高標記編號的用于不太常用的可選元素。重復字段中的每個元素都需要重新編碼標記號,因此重復字段特別適合此優(yōu)化。
每個字段都必須使用以下修飾符之一進行注解:
optional
:該字段可以設置,也可以不設置。如果未設置可選字段值,則使用默認值。對于簡單類型,可以指定自己的默認值,就像示例中為電話號碼type
所做的那樣。否則,將使用系統默認值:數字類型的默認值為零,字符串類型的默認值為空字符串,布爾類型的默認值為false
。對于嵌入式消息,默認值始終是消息的“默認實例”或“原型”,其沒有設置任何字段。調用訪問器以獲取尚未顯式設置的可選(或必需)字段的值時,始終返回該字段的默認值。repeated
:該字段可以重復任意多次(包括零次),表示該字段可以包含多個值。將重復字段視為動態(tài)大小的數組,重復值的順序將在協議緩沖區(qū)中保留。required
:必須提供該字段的值,否則該消息將被視為“未初始化”。序列化未初始化的消息將引發(fā)異常。解析未初始化的消息將失敗。除此之外,必需字段的行為與可選字段完全相同。
重要
required
是永久的,在將字段標記為required
時應非常小心。如果在某個時候希望停止編寫或發(fā)送必需字段,將該字段更改為可選字段將很成問題 - 舊的讀取器會認為沒有此字段的消息不完整,并可能會意外地拒絕或刪除它們。你應該考慮為協議緩沖區(qū)編寫特定于應用程序的自定義驗證例程。在Google 強烈不贊成使用required
字段;在 proto2 語法中定義的大多數消息僅使用optional
和repeated
。(Proto3 根本不支持required
字段。)
編譯協議緩沖區(qū)
現在有了.proto
,接下來需要做的就是生成讀寫 AddressBook
(以及 Person
和 PhoneNumber
)消息所需的類。為此,需要在 .proto
上運行協議緩沖區(qū)編譯器 protoc
:
1、下載protoc
后解壓,將protoc
所在bin
目錄路徑添加到系統環(huán)境變量
>protoc --version libprotoc 25.4
2、現在運行編譯器,指定源目錄(應用程序源代碼所在的位置 - 如果未提供值,則使用當前目錄)、目標目錄(希望生成的代碼的存儲目錄;通常與 $SRC_DIR
相同)和 .proto
的路徑。如下:
protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto
因為想要 Python 類,所以使用 --python_out
選項 - 為其他受支持的語言提供了類似的選項。
protoc
還可以使用--pyi_out
生成python存根(.pyi)。
這會在你指定的目標目錄中生成對應的xxxx_pb2.py
實踐:cmd打開控制臺,進入到addressbook.proto3
所在目錄,然后執(zhí)行以下命令
protoc --python_out=. addressbook.proto2
命令執(zhí)行成功后,會再當前目錄下生成與.proto2
文件同名目錄(例中為addressbook
),目錄下自動生成對應的py文件(例中為proto2_pb2.py
,實踐時將其拷貝到addressbook.proto2
所在目錄并從命名為addressbook_pb2.py
)
協議緩沖區(qū) API
與生成 Java 和 C++ 協議緩沖區(qū)代碼不同,Python 協議緩沖區(qū)編譯器不會直接為你生成數據訪問代碼。相反(如果你查看 addressbook_pb2.py
,你就會看到),它會為你的所有消息、枚舉和字段生成特殊描述符,以及一些神秘的空類,每個消息類型一個類。
# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: addressbook.proto2 # Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12\x61\x64\x64ressbook.proto2\x12\x08tutorial\"\xa3\x02\n\x06Person\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\x05\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12,\n\x06phones\x18\x04 \x03(\x0b\x32\x1c.tutorial.Person.PhoneNumber\x1aX\n\x0bPhoneNumber\x12\x0e\n\x06number\x18\x01 \x01(\t\x12\x39\n\x04type\x18\x02 \x01(\x0e\x32\x1a.tutorial.Person.PhoneType:\x0fPHONE_TYPE_HOME\"h\n\tPhoneType\x12\x1a\n\x16PHONE_TYPE_UNSPECIFIED\x10\x00\x12\x15\n\x11PHONE_TYPE_MOBILE\x10\x01\x12\x13\n\x0fPHONE_TYPE_HOME\x10\x02\x12\x13\n\x0fPHONE_TYPE_WORK\x10\x03\"/\n\x0b\x41\x64\x64ressBook\x12 \n\x06people\x18\x01 \x03(\x0b\x32\x10.tutorial.Person') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'addressbook.proto2_pb2', _globals) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _globals['_PERSON']._serialized_start=33 _globals['_PERSON']._serialized_end=324 _globals['_PERSON_PHONENUMBER']._serialized_start=130 _globals['_PERSON_PHONENUMBER']._serialized_end=218 _globals['_PERSON_PHONETYPE']._serialized_start=220 _globals['_PERSON_PHONETYPE']._serialized_end=324 _globals['_ADDRESSBOOK']._serialized_start=326 _globals['_ADDRESSBOOK']._serialized_end=373 # @@protoc_insertion_point(module_scope)
每個類中的重要行是 __metaclass__ = reflection.GeneratedProtocolMessageType
??梢詫⑺鼈円暈閯?chuàng)建類的模板。在加載時,GeneratedProtocolMessageType
元類使用指定的描述符來創(chuàng)建使用每種消息類型所需的所有 Python 方法,并將它們添加到相關的類中。然后可以在代碼中使用完全填充的類。
所有這一切的最終效果是,你可以使用 Person
類,就好像它將 Message 基類的每個字段定義為常規(guī)字段一樣。例如:
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.PHONE_TYPE_HOME
注意,這些賦值不僅僅是向通用 Python 對象添加任意新字段。如果你嘗試分配 .proto 文件中未定義的字段,則會引發(fā) AttributeError
。如果你將字段分配給錯誤類型的值,則會引發(fā) TypeError
。此外,在設置字段之前讀取字段的值會返回默認值。
枚舉
元類將枚舉擴展為一組具有整數值的符號常量。因此,例如,常量 addressbook_pb2.Person.PhoneType.PHONE_TYPE_WORK
的值為 2。
標準消息方法
每個消息類還包含許多其他方法,讓你可以檢查或操作整個消息,包括:
IsInitialized()
: 檢查是否已設置所有必需的字段。__str__()
:返回消息的可讀表示形式,特別適用于調試。(通常這樣調用str(message)
或print(message)
)CopyFrom(other_msg)
:使用給定消息的值覆蓋消息。Clear()
:清除所有元素,使其返回到空狀態(tài)。
這些方法實現了 Message 接口。有關更多信息,請參閱 Message 的完整 API 文檔。
解析和序列化
每個協議緩沖區(qū)類都具有使用協議緩沖區(qū)二進制格式來寫入和讀取所選類型消息的方法。這些方法包括:
SerializeToString()
:序列化消息并將其作為字符串返回。注意,bytes是二進制的,不是文本;僅將str
類型用作方便的容器。ParseFromString(data)
:從給定的字符串解析消息。
這些只是用于解析和序列化的選擇中的一部分。同樣,請參閱Message API 參考以獲取完整列表。
重要
協議緩沖區(qū)和面向對象設計 協議緩沖區(qū)類基本上是數據持有者(如 C 中的結構),不提供其他功能;它們在對象模型中不是好的首要公民。如果想為生成的類添加更豐富的行為,最好的方法是將生成的協議緩沖區(qū)類包裝在特定于應用程序的類中。如果你無法控制
.proto
文件的設計(例如,如果正在復用來自另一個項目的文件),那么包裝協議緩沖區(qū)也是一個好主意。在這種情況下,您可以使用包裝器類來構建更適合你應用程序的獨特環(huán)境的接口:隱藏一些數據和方法,公開便捷功能等。絕不應通過繼承生成的類繼承來向它們添加行為。這會破壞內部機制,而且無論如何也不是好的面向對象實踐。
編寫消息
假設希望通訊錄應用程序能夠做到的第一件事就是將個人詳細信息寫入通訊錄文件。為此,需要創(chuàng)建和填充協議緩沖區(qū)類的實例,然后將它們寫入輸出流。
這里示例代碼從文件中讀取 AddressBook
,根據用戶輸入向其中添加一個新 Person
,然后將新的 AddressBook
再次寫回文件。直接調用或引用協議編譯器生成的代碼的部分已突出顯示。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import addressbook_pb2 import os def PromptForAddress(person): '''基于用戶輸入填充Person消息''' person.id = int(input('Enter person ID number: ')) person.name = input('Enter name: ') email = input('Enter email address (blank for none): ') if email != '': person.email = email while True: number = input('Enter a phone number (or leave blank to finish): ') if number == '': break phone_number = person.phones.add() phone_number.number = number phone_type = input('Is this a mobile, home, or work phone? ') if phone_type == 'mobile': phone_number.type = addressbook_pb2.Person.PhoneType.PHONE_TYPE_MOBILE elif phone_type == 'home': phone_number.type = addressbook_pb2.Person.PhoneType.PHONE_TYPE_HOME elif phone_type == 'work': phone_number.type = addressbook_pb2.Person.PhoneType.PHONE_TYPE_WORK else: print('Unknown phone type; leaving as default value.') address_book = addressbook_pb2.AddressBook() # 讀取已存在地址簿 if os.path.exists('my_addressbook.db'): with open('my_addressbook.db', 'rb') as f: address_book.ParseFromString(f.read()) # 添加一個通訊地址 PromptForAddress(address_book.people.add()) # 將通訊地址寫到磁盤 with open('my_addressbook.db', 'wb') as f: f.write(address_book.SerializeToString())
運行程序后按提示輸入內容,形如以下
Enter person ID number: 1 Enter name: shouke Enter email address (blank for none): shouke@163.com Enter a phone number (or leave blank to finish): 15813735565 Is this a mobile, home, or work phone? mobile Enter a phone number (or leave blank to finish):
讀取消息
此示例讀取上述示例創(chuàng)建的文件,并打印其中的所有信息
# -*- coding:utf-8 -*- import addressbook_pb2 def ListPeople(address_book): '''遍歷地址簿中的所有people并打印相關信息''' 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.PHONE_TYPE_MOBILE: print('Mobile phone #: ', end='') elif phone_number.type == addressbook_pb2.Person.PhoneType.PHONE_TYPE_HOME: print('Home phone #: ', end='') elif phone_number.type == addressbook_pb2.Person.PhoneType.PHONE_TYPE_WORK: print('Work phone #: ', end='') print(phone_number.number) address_book = addressbook_pb2.AddressBook() # 讀取已存在地址簿 with open('my_addressbook.db', 'rb') as f: address_book.ParseFromString(f.read()) ListPeople(address_book)
運行輸出:
Person ID: 1 Name: shouke E-mail address: shouke@163.com Mobile phone #: 15813735565
另一個示例
例子中,定義了一個名為Device
的消息,它有4個字段:name
、price
,type
和labels
。
device.proto
syntax = "proto3"; message Device { string name = 1; int32 price = 2; string type = 3; map<string, string> labels = 15; }
根據device.proto
文件生成python文件
protoc --python_out=. device.proto
自動在當前目錄下生成device
目錄及device/proto3_pb2.py
文件
使用生成的py文件(拷貝上述py文件并重命名為device_pb2.py
,和以下文件存放在同級目錄)
my_test.py
# -*- coding:utf-8 -*- import device_pb2 # 創(chuàng)建一個Person對象并設置字段值 device = device_pb2.Device() device.name = '聯想小星' device.price = 3999 device.type = 'Notebook' device.labels['color'] = 'red' device.labels['outlook'] = 'fashionable' # 序列化Person對象為二進制字符串 serialized_device = device.SerializeToString() print(f"序列化后的數據:{serialized_device}") # 反序列化二進制字符串為一個新的Person對象 new_device = device_pb2.Device() new_device.ParseFromString(serialized_device) # 輸出新的Device對象的字段值 print(type(new_device.labels)) # <class 'google._upb._message.ScalarMapContainer'> for label, value in new_device.labels.items(): print(label, value) # 輸出內容形如:color red print(new_device.labels) # {'color': 'red', 'outlook': 'fashionable'} print(f'反序列化后的數據:設備名稱={new_device.name}, 價格={new_device.price}, 類型={new_device.type}, 標簽={new_device.labels}') # 輸出:反序列化后的數據:設備名稱=聯想小星, 價格=3999, 類型=Notebook, 標簽={'color': 'red', 'outlook': 'fashionable'}
參考鏈接
https://protobuf.dev/getting-started/pythontutorial/
https://protobuf.com.cn/getting-started/pythontutorial/
https://protobuf.dev/programming-guides/proto3/
到此這篇關于在Python中使用Protocol Buffers基礎介紹的文章就介紹到這了,更多相關Python使用Protocol Buffers內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
python獲取的html中都是\\u003e實現轉成正確字符
這篇文章主要介紹了python獲取的html中都是\\u003e實現轉成正確字符方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07python+tkinter+mysql做簡單數據庫查詢界面
本文主要介紹了python+tkinter+mysql做簡單數據庫查詢界面,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-01-01Python 使用PyQt5 完成選擇文件或目錄的對話框方法
今天小編就為大家分享一篇Python 使用PyQt5 完成選擇文件或目錄的對話框方法。具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-06-06