つかびーの技術日記

情報系修士卒のWeb系技術日記です。現在のフォーカス分野はアドテクです。

PythonでProtocol Buffers3を使う&悩みどころを解説

   

こんにちは、@s_tsukaです。

最近PythonとProtocol Buffers3を使ってクラスの自動生成、Serialize/Deserializeを行いました。

せっかくなのでそれらの使い方と難しいところ・悩みどころをメモしておきます。

Protocol Buffers3

まずはProtocol Buffers (protobuf) について簡単に。

https://developers.google.com/protocol-buffers/

https://github.com/google/protobuf

Protocol BuffersはGoogleが開発したメッセージフォーマットです。フォーマットなのでcsv, json, avro, parquetなどの親戚とでも思っておけば良いです。メッセージはバイナリ形式なので転送効率が良くなるという代物です。

単にメッセージのフォーマット、というだけでなくメッセージを定義するSchemaとしての役目もあります。これについてはあとで使い方の節で解説します。

現在メインで使われているのはver2ですが、今回はver3を使います。

https://developers.google.com/protocol-buffers/docs/proto3

使い方

ここからは実際にprotobuf3のSchemaを定義して、そこからPythonクラスを自動生成して使ってみます。

ほぼこちら(https://developers.google.com/protocol-buffers/docs/pythontutorial)と同じですが、こっちはver2の定義なので、一部変更して使います。

まずはprotocコマンドを利用できるようにしておきます。

homebrewの人はこんな感じで。その他の環境は公式docを御覧ください。

次に生成するクラスのschemaを作成します。

このファイルを生成したあとで以下のコマンドを実行します。

addressbook_pb2.pyというファイルが生成されるので、あとはこれをimportすれば生成したクラスが利用できるようになります。

早速使ってみましょう。

ちゃんと使えました。APIがProtobufのものになるのでadd()とか若干トリッキーな気もしますが・・・

protobuf pythonにはParseとMessageToJsonという関数が用意されています。Parseを使うとjson stringからprotobuf message objectへ、MessageToJsonを使うとprotobuf message objectからjson stringへ変換ができます。

簡単ですね。

悩みどころ

ここからは自分が困ったことについて書いていきます。

空と零を区別したい(フィールドが消されてしまう)

上記でpersonを作成しました。このとき、nameしか値を設定しなかったのでjsonもその通りnameフィールドだけになっています。これはまあそうですよね。

しかし時にはid, emailに値を設定しなかったとしても、それらのフィールドも0と”(空文字列)としてjson出力してほしいときがあります。

そんなときはMessageToJsonincluding_default_value_fieldsをTrueにしましょう。

idやemailが0や”で出力されました。

一見これでいいともおもえるのですが、このPerson jsonを受け付けるAPIサーバがあったとしてそれが以下のようなバリデーションの仕様だとしたらどうでしょうか。(実際にあった・・・)

  1. idは0でもよいが必ずなければならない
  2. emailはフィールドは存在しないまたは1文字以上なら良い (0文字はNG)

including_default_value_fieldsはFalseにしたままで、emailフィールドを設定しないでidフィールドだけ0に設定すれば良いのでは?と思います。普通そう考えるはずです。

p.id = 0をしているにもかかわらず結果はidフィールドがありません。

including_default_value_fieldsをTrueにしてしまうとidは解決できますが、今度はemailが空文字列になってしまってNGです。あちらを立てればこちらが立たず・・・

これについては我らが@xuwei_k先生が助言をくれました。ありがとう・・・!Scalaの話じゃないのに教えてくれてほんとありがとう!

これについて調べたら@matsu_charaさんのこちらの記事が大変参考になりました。

ProtocolBuffersでprimitiveのデフォルト値と値が入っていないことを区別したいときにどう書くか

idフィールドは必ずjsonにマッピングしてほしいので、そこにgoogle.protobuf.Int32Valueを使います。

これでちゃんとemailはないけどidはある、という状況を作れました。

ところがこれも完璧ではなくて、p.id.value = 0というようにvalueという余計なものが入ってきています。wrapper typeを使う以上仕方ない・・・かな

中途半端にフィールドがある状態になる

先程と同様ですが、これもjsonへのdeserializeで苦労した点です。

少しschemaを変えます。

若干不自然ですが・・・会社が本社を持っていて、本社は場所を持っていて、場所は通称を複数持っているというネストしている構造です。

このjsonをparseしてobjにしてから再度jsonにしてみます。

元のjsonのpopular_nameが空配列だったにもかかわらず、protobuf messageの段階で消えています・・。これは・・どうしたらいいんでしょうか。

実際こういうリクエストを弾いてくるAPIサーバがあって、苦労しました。これについては未だに解決策がわかっていません。

まとめ

  • サンプルの通り、割と簡単にprotobufを利用できます
  • 空と零の区別はwrapper typeを使いましょう
  • Parse関数は上記の通り完璧にparseしてくれるわけではないので注意が必要です

 

 - Python , ,