用 Python 學 Google Protocol Buffers - Part 3

Posted on  Nov 9, 2018  in  Python 程式設計 - 高階  by  Amo Chen  ‐ 3 min read

本文為系列教學:

上篇文章介紹 Google Protocols Buffers 介紹 proto3 幾個重要語法與特性,本篇將介紹如何撰寫更複雜的 message ,以及幾種方便的資料型態與其使用方法。

複合式 message

一般來說 message 的定義很可能會像 JSON 字串一樣, 1 個 object 內又包著 1 個 object ,所以也會有 message 內包著其他 message 的情況,例如購物車內可能會包含各種產品:

syntax = "proto3";

message User {
	string id = 1;
	string name = 2;
}

message Product {
	string id = 1;
	string name = 2;
	int32 price = 3;
	int32 quantity = 4;
}

message Cart {
	User user = 1;
	repeated Product products = 2;
}

複合式的 message 就是先定義最小的 message 之後,再由這些小 message 組合而成。

編譯成 Python 之後就能夠用 MergeFrom 或是 add (add 限 repeated 的欄位)等方法進行組裝:

>>> from cart_pb2 import User, Product, Cart
>>> user = User()
>>> user.id = "userid"
>>> user.name = "username"
>>>
>>> cart = Cart()
>>> cart.user.MergeFrom(user)
>>> product = cart.products.add()
>>> product.id = "productid"
>>> product.name = "productname"
>>> product.price = 100
>>> product.quantity = 1
>>>
>>> cart.SerializeToString()
b'\n\x12\n\x06userid\x12\x08username\x12\x1c\n\tproductid\x12\x0bproductname\x18d \x01'

如果是從 binary 資料想直接組裝的話,則可以使用 MergeFromString

>>> product_string = product.SerializeToString()
>>> product_2 = cart.products.add()
>>> product_2.MergeFromString(product_string)
>>> print(len(cart.products))
2
>>> cart.SerializeToString()
b'\n\x12\n\x06userid\x12\x08username\x12\x1c\n\tproductid\x12\x0bproductname\x18d \x01\x12\x1c\n\tproductid\x12\x0bproductname\x18d \x01'

以上就是複合式 message 的使用方法,是否相當簡單?

import .proto

Google Protocol Buffers 可以將 message 拆成不同的 .proto 檔,再用 import 的方式引入:

syntax "proto3";

import "user.proto";

message Product {
	string id = 1;
	string name = 2;
	int32 price = 3;
	int32 quantity = 4;
}

message Cart {
	User user = 1;
	repeated Product products = 2;
}

上述範例可以看到 User 是透過 import "user.proto"; 引入的。

Any 型別

偶爾可能也會遇到 message 欄位可能會存放各式各樣資料的情況,例如日誌類型的資料不見得有固定的格式,這種情況下可以試著使用 proto3 所提供的擴充型別 - Any

使用 Any 型別需要透過 import "google/protobuf/any.proto"; 引用。

以下範例試著定義 1 個 message type 稱為 Event ,其中欄位 payloadAny ,用以裝載各式各樣的 message type:

syntax = "proto3";

import "google/protobuf/any.proto";

message Event {
	string event = 1;
	google.protobuf.Any payload = 2;
}

以下為操作 Event 的 Python 範例:

>>> from event_pb2 import Event
>>> from cart_pb2 import User
>>>
>>> event = Event()
>>> event.type = "viewProductPage"
>>> user = User()
>>> user.id = "userid"
>>> user.name = "username"
>>> event.payload.Pack(user)
>>> event.payload
type_url: "type.googleapis.com/User"
value: "\n\006userid\022\010username"

上述範例可以看到 Any 型別是用 Pack() 方法將 message 放入欄位之中,如果試圖使用 MergeFrom() 就只能使用同是 Any 型別的 message ,否則會出現類似以下的錯誤:

TypeError: Parameter to MergeFrom() must be instance of same class: expected google.protobuf.Any got User.

pack() 裝載之後,如果要解讀 Any 型別的欄位,就得透過 Unpack() 方法:

>>> if event.payload.Is(User.DESCRIPTOR):
...    user2 = User()
...    event.payload.Unpack(user2)
...    print(user2)

id: "userid"
name: "username"

上述範例透過 Is() 方法判斷 payload 欄位是否存放 User 型別的資料,接著新增 1 個 user2 並用 Unpack(user2) 將資料 deserialize 至 user2 。

以上就是 Any 型別的介紹。

其他擴充型別

proto3 新增許多擴充型別,例如 Maps , Timestamp , Duration 等等,增加不少實用性與方便性。

以下為 Maps 的宣告格式,需要各自指定 key 與 value 的型別:

map<key_type, value_type> map_field = N;

例如,宣告一個 key 為字串, value 為 User 的 Map:

syntax = "proto3";

message Mapping {
	map<string, User> id_user_map = 1;
}

編譯後的使用範例為:

>>> from cart_pb2 import User
>>> from mapping_pb2 import Mapping
>>>
>>> mapping = Mapping()
>>>
>>> user1 = mapping.id_user_map.get_or_create("userid_1")
>>> user1.id = 'userid_1'
>>>
>>> user2 = mapping.id_user_map.get_or_create("userid_2")
>>> user2.id = 'userid_2'
>>>
>>> for k, v in mapping.id_user_map.keys():
...    print(k)
...
userid_1
userid_2

上述範例可以看到 map 是透過 get_or_create(key) 新增資料,如果 key 值已存在於 map 中就會取得該值,沒有的話就會新增一組。 map 也可以透過 keys() 走訪已存在的 key 值。

目前其他資料型態的使用方法,在 Google Protocol Buffers 官網上並沒有太多範例,但可以進一步詳閱 protocolbuffers/protobuf Github 中的 .proto 檔,其檔案註解中有使用範例能夠參考。

MessageToJson 將 Message 轉成 JSON

proto3 也增加資料交換的方便性(畢竟許多應用仍使用 JSON 作為資料交換格式),支援將 message 轉成 JSON 字串, 各種資料型態與 JSON 資料型態對照表請參考 JSON Mapping 一表。

以下範例為如何在 Python 中將 message 轉為 JSON:

>>> import google.protobuf.json_format
>>> print(google.protobuf.json_format.MessageToJson(mapping))
{
  "idUserMap": {
    "userid_2": {
      "id": "userid_2"
    },
    "userid_1": {
      "id": "userid_1"
    }
  }
}

結語

針對 Google Protocol Buffers 介紹就到此完結,希望大家看完此系列文章後,都能夠對 Google Protocol Buffers 有基礎的認識。

References

https://developers.google.com/protocol-buffers/docs/reference/python-generated

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!