Protocol Buffer の基本:Python
このチュートリアルでは、Pythonプログラマー向けにプロトコルバッファの基本的な扱い方を紹介します。簡単なサンプルアプリケーションを作成する手順を通して、次の方法を説明します。
.proto
ファイルでメッセージ形式を定義する。- プロトコルバッファコンパイラを使用する。
- Python プロトコルバッファ API を使用してメッセージを書き込み、読み込む。
これはPythonでプロトコルバッファを使用するための包括的なガイドではありません。より詳細なリファレンス情報については、Protocol Buffer 言語ガイド (proto2)、Protocol Buffer 言語ガイド (proto3)、Python API リファレンス、Python 生成コードガイド、およびエンコーディングリファレンスを参照してください。
問題領域
ここで使用する例は、人々の連絡先情報をファイルに読み書きできる、ごくシンプルな「アドレス帳」アプリケーションです。アドレス帳の各人物は、名前、ID、メールアドレス、連絡先の電話番号を持っています。
このような構造化されたデータをどのようにシリアル化し、取得するのでしょうか?この問題を解決するには、いくつか方法があります。
- Python のピクルス機能を使用する。これは言語に組み込まれているためデフォルトのアプローチですが、スキーマの進化にうまく対応できず、C++ や Java で書かれたアプリケーションとデータを共有する必要がある場合にもあまりうまく機能しません。
- データ項目を単一の文字列にエンコードするアドホックな方法(例:「12:3:-23:67」のように4つの整数をエンコードする)を考案することもできます。これはシンプルで柔軟なアプローチですが、一度限りのエンコードおよびパースコードの作成が必要であり、パースにはわずかな実行時コストがかかります。これはごくシンプルなデータのエンコードに最適です。
- データをXMLにシリアル化する。XMLは(ある程度)人間が読める形式であり、多くの言語に対応するバインディングライブラリがあるため、このアプローチは非常に魅力的です。他のアプリケーションやプロジェクトとデータを共有したい場合には良い選択肢となります。しかし、XMLはデータ容量が大きくなることで悪名高く、そのエンコード/デコードはアプリケーションに大きなパフォーマンスペナルティを課す可能性があります。また、XML DOMツリーをナビゲートすることは、通常のクラスのシンプルなフィールドをナビゲートするよりもかなり複雑です。
これらの選択肢の代わりに、プロトコルバッファを使用できます。プロトコルバッファは、この問題を正確に解決するための柔軟で効率的、自動化されたソリューションです。プロトコルバッファを使用すると、保存したいデータ構造の.proto
記述を作成します。そこから、プロトコルバッファコンパイラは、効率的なバイナリ形式でプロトコルバッファデータの自動エンコーディングとパースを実装するクラスを作成します。生成されたクラスは、プロトコルバッファを構成するフィールドのゲッターとセッターを提供し、プロトコルバッファを単位として読み書きする詳細を処理します。重要なのは、プロトコルバッファ形式が、古い形式でエンコードされたデータをコードが読み取れるように、時間の経過とともに形式を拡張するという考え方をサポートしていることです。
サンプルコードの場所
サンプルコードは、ソースコードパッケージの「examples」ディレクトリに含まれています。こちらからダウンロードしてください。
プロトコル形式の定義
アドレス帳アプリケーションを作成するには、まず.proto
ファイルから始める必要があります。.proto
ファイル内の定義はシンプルです。シリアル化したい各データ構造に対してメッセージを追加し、次にメッセージ内の各フィールドに名前と型を指定します。これがメッセージを定義する.proto
ファイル、addressbook.proto
です。
edition = "2023";
package tutorial;
message Person {
string name = 1;
int32 id = 2;
string email = 3;
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2 [default = PHONE_TYPE_HOME];
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
ご覧のとおり、構文はC++やJavaに似ています。ファイルの各部分を順に見て、それぞれの役割を確認しましょう。
.proto
ファイルはパッケージ宣言から始まります。これは異なるプロジェクト間の名前の衝突を防ぐのに役立ちます。Pythonでは、パッケージは通常ディレクトリ構造によって決定されるため、.proto
ファイルで定義するpackage
は生成されたコードには影響しません。しかし、Protocol Buffersの名前空間およびPython以外の言語での名前の衝突を避けるためには、依然として宣言すべきです。
次に、メッセージ定義があります。メッセージは、型付きフィールドのセットを含む単なる集約体です。bool
、int32
、float
、double
、string
など、多くの標準的な単純なデータ型がフィールド型として利用できます。また、他のメッセージ型をフィールド型として使用することで、メッセージにさらなる構造を追加することもできます。上記の例では、Person
メッセージにはPhoneNumber
メッセージが含まれ、AddressBook
メッセージにはPerson
メッセージが含まれています。メッセージ型を他のメッセージ内にネストして定義することもできます。ご覧のとおり、PhoneNumber
型はPerson
内に定義されています。フィールドのいずれかに事前定義された値のリストのいずれかを持たせたい場合は、enum
型を定義することもできます。ここでは、電話番号が以下の電話タイプのうちのいずれかであることを指定したいとします: PHONE_TYPE_MOBILE
、PHONE_TYPE_HOME
、またはPHONE_TYPE_WORK
。
各要素の「= 1」、「= 2」のマーカーは、バイナリエンコーディングでそのフィールドが使用する一意の「タグ」を識別します。タグ番号1~15は、それよりも大きい番号よりも1バイト少なくエンコードされるため、最適化として、これらのタグをよく使われる要素や繰り返される要素に使用し、16以上のタグはあまり使われないオプションの要素に残すことができます。繰り返しフィールドの各要素はタグ番号の再エンコードを必要とするため、繰り返しフィールドはこの最適化に特に適しています。
可能なフィールド型を含む.proto
ファイルの作成に関する完全なガイドは、Protocol Buffer 言語ガイドにあります。ただし、クラス継承に似た機能を探すのはやめましょう。プロトコルバッファはそれを行いません。
プロトコルバッファのコンパイル
.proto
ファイルができたので、次に必要なのは、AddressBook
(したがってPerson
とPhoneNumber
)メッセージを読み書きするために必要なクラスを生成することです。これを行うには、.proto
ファイルに対してプロトコルバッファコンパイラprotoc
を実行する必要があります。
コンパイラをインストールしていない場合は、パッケージをダウンロードして、READMEの指示に従ってください。
次に、コンパイラを実行し、ソースディレクトリ(アプリケーションのソースコードがある場所 — 値を指定しない場合は現在のディレクトリが使用されます)、出力先ディレクトリ(生成されたコードを配置する場所。多くの場合
$SRC_DIR
と同じ)、および.proto
ファイルへのパスを指定します。この場合、あなたは…protoc --proto_path=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto
Python クラスが必要なため、
--python_out
オプションを使用します。他のサポートされている言語にも同様のオプションが用意されています。Protocは
--pyi_out
を使用するとpythonスタブ(.pyi
)も生成できます。
これにより、指定した出力先ディレクトリにaddressbook_pb2.py
(またはaddressbook_pb2.pyi
)が生成されます。
プロトコルバッファAPI
JavaやC++のプロトコルバッファコードを生成する場合とは異なり、Pythonプロトコルバッファコンパイラはデータアクセスコードを直接生成しません。代わりに(addressbook_pb2.py
を見るとわかるように)、すべてのメッセージ、enum、フィールドに特殊な記述子を生成し、各メッセージタイプに対して不思議なほど空のクラスを生成します。
import google3
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.GOOGLE_INTERNAL,
0,
20240502,
0,
'',
'main.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nmain.proto\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, 'google3.main_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_PERSON']._serialized_start=25
_globals['_PERSON']._serialized_end=316
_globals['_PERSON_PHONENUMBER']._serialized_start=122
_globals['_PERSON_PHONENUMBER']._serialized_end=210
_globals['_PERSON_PHONETYPE']._serialized_start=212
_globals['_PERSON_PHONETYPE']._serialized_end=316
_globals['_ADDRESSBOOK']._serialized_start=318
_globals['_ADDRESSBOOK']._serialized_end=365
# @@protoc_insertion_point(module_scope)
各クラスにおける重要な行は__metaclass__ = reflection.GeneratedProtocolMessageType
です。Pythonのメタクラスがどのように機能するかの詳細は本チュートリアルの範囲外ですが、それらをクラス作成のためのテンプレートのようなものだと考えることができます。ロード時に、GeneratedProtocolMessageType
メタクラスは指定された記述子を使用して、各メッセージタイプを扱うために必要なすべてのPythonメソッドを作成し、それらを関連するクラスに追加します。その後、コード内で完全に埋められたクラスを使用できます。
これらすべてがもたらす最終的な効果は、Person
クラスを、Message
基底クラスの各フィールドを通常のフィールドとして定義しているかのように使用できるということです。たとえば、次のように記述できます。
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
ファイルで定義されていないフィールドを代入しようとすると、AttributeError
が発生します。間違った型の値をフィールドに代入すると、TypeError
が発生します。また、フィールドが設定される前にその値を読み取ると、デフォルト値が返されます。
person.no_such_field = 1 # raises AttributeError
person.id = "1234" # raises TypeError
プロトコルコンパイラが特定のフィールド定義に対してどのようなメンバーを生成するかについての詳細は、Python 生成コードリファレンスを参照してください。
Enums
Enums はメタクラスによって、整数値を持つ一連のシンボリック定数に展開されます。したがって、たとえば定数addressbook_pb2.Person.PhoneType.PHONE_TYPE_WORK
は値2を持ちます。
標準メッセージメソッド
各メッセージクラスには、メッセージ全体をチェックしたり操作したりするための、次のような他のメソッドも含まれています。
IsInitialized()
: 必要なフィールドがすべて設定されているかを確認します。__str__()
: メッセージの人間が読める表現を返します。特にデバッグに役立ちます。(通常、str(message)
またはprint message
として呼び出されます。)CopyFrom(other_msg)
: 指定されたメッセージの値でメッセージを上書きします。Clear()
: すべての要素を空の状態に戻します。
これらのメソッドはMessage
インターフェースを実装しています。詳細については、Message
の完全なAPIドキュメントを参照してください。
パースとシリアル化
最後に、各プロトコルバッファクラスには、選択したタイプのメッセージをプロトコルバッファのバイナリ形式を使用して読み書きするためのメソッドがあります。これらには、次が含まれます。
SerializeToString()
: メッセージをシリアル化し、文字列として返します。バイトはバイナリであり、テキストではないことに注意してください。便宜上、str
型をコンテナとして使用しているに過ぎません。ParseFromString(data)
: 指定された文字列からメッセージをパースします。
これらは、パースとシリアル化のために提供されているオプションの一部にすぎません。完全なリストについては、Message
APIリファレンスを再度参照してください。
重要
プロトコルバッファとオブジェクト指向設計 プロトコルバッファクラスは、基本的には追加の機能を提供しないデータホルダー(C言語の構造体のようなもの)であり、オブジェクトモデルにおいて優れた第一級市民ではありません。生成されたクラスに豊かな動作を追加したい場合、最適な方法は、生成されたプロトコルバッファクラスをアプリケーション固有のクラスでラップすることです。プロトコルバッファのラップは、.proto
ファイルの設計を制御できない場合(例えば、他のプロジェクトのものを再利用している場合など)にも良いアイデアです。その場合、ラッパークラスを使用して、アプリケーションの独自の環境により適したインターフェースを作成できます。一部のデータとメソッドを隠蔽したり、便利な関数を公開したりするなどです。生成されたクラスを継承して動作を追加すべきではありません。これは内部メカニズムを破壊し、いずれにせよ優れたオブジェクト指向の実践ではありません。メッセージの書き込み
次に、プロトコルバッファクラスを使ってみましょう。アドレス帳アプリケーションで最初にできるようにしたいのは、個人情報をアドレス帳ファイルに書き込むことです。これを行うには、プロトコルバッファクラスのインスタンスを作成してデータを格納し、それらをストリームに出力する必要があります。
以下は、ファイルからAddressBook
を読み込み、ユーザー入力に基づいて新しいPerson
を1人追加し、新しいAddressBook
を再びファイルに書き戻すプログラムです。プロトコルコンパイラによって生成されたコードを直接呼び出したり参照したりする部分は強調表示されています。
#!/usr/bin/env python3
import addressbook_pb2
import sys
# This function fills in a Person message based on user input.
def PromptForAddress(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.")
# 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:
with open(sys.argv[1], "rb") as f:
address_book.ParseFromString(f.read())
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.
with open(sys.argv[1], "wb") as f:
f.write(address_book.SerializeToString())
メッセージの読み込み
もちろん、アドレス帳から何の情報を得ることもできないなら、あまり役に立ちません!この例は、上記の例で作成されたファイルを読み込み、そこにあるすべての情報を出力します。
#!/usr/bin/env python3
import 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.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)
# 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.
with open(sys.argv[1], "rb") as f:
address_book.ParseFromString(f.read())
ListPeople(address_book)
プロトコルバッファの拡張
プロトコルバッファを使用するコードをリリースした後、遅かれ早かれ、プロトコルバッファの定義を「改善」したくなるでしょう。新しいバッファが後方互換性を持ち、古いバッファが前方互換性を持つことを望む場合(そして、ほとんどの場合そう望むはずです)、従うべきいくつかのルールがあります。プロトコルバッファの新しいバージョンでは、
- 既存のフィールドのタグ番号を変更してはなりません。
- 必須フィールドを追加または削除してはなりません。
- オプションまたは繰り返しフィールドを削除できます。
- 新しいオプションまたは繰り返しフィールドを追加できますが、そのプロトコルバッファでこれまでに使用されていない(削除されたフィールドによっても使用されていない)新しいタグ番号を使用する必要があります。
(これらのルールにはいくつかの例外がありますが、めったに使用されません。)
これらのルールに従えば、古いコードは新しいメッセージを問題なく読み込み、新しいフィールドは単純に無視します。古いコードにとって、削除されたオプションフィールドは単にデフォルト値になり、削除された繰り返しフィールドは空になります。新しいコードも古いメッセージを透過的に読み込みます。ただし、新しいオプションフィールドは古いメッセージには存在しないため、has_
で明示的に設定されているかどうかを確認するか、タグ番号の後に[default = value]
を使用して.proto
ファイルで適切なデフォルト値を提供する必要があることに注意してください。オプション要素のデフォルト値が指定されていない場合、型固有のデフォルト値が使用されます。文字列の場合、デフォルト値は空の文字列です。ブール値の場合、デフォルト値はfalseです。数値型の場合、デフォルト値はゼロです。また、新しい繰り返しフィールドを追加した場合、新しいコードはそれが空にされたのか(新しいコードによって)、まったく設定されなかったのか(古いコードによって)を判断できないことに注意してください。これは、それに対してhas_
フラグがないためです。
高度な使用法
プロトコルバッファには、単純なアクセサーやシリアル化を超えた用途があります。Python API リファレンスを調べて、他に何ができるかを確認してください。
プロトコルメッセージクラスが提供する主要な機能の1つはリフレクションです。特定のメッセージタイプにコードを記述することなく、メッセージのフィールドを反復処理し、その値を操作できます。リフレクションを使用する非常に便利な方法の1つは、プロトコルメッセージをXMLやJSONなどの他のエンコーディングに変換することです。リフレクションのより高度な使用法としては、同じタイプの2つのメッセージ間の違いを見つけたり、特定のメッセージコンテンツに一致する式を記述できる一種の「プロトコルメッセージの正規表現」を開発したりすることが考えられます。想像力を働かせれば、プロトコルバッファを当初予想していたよりもはるかに幅広い問題に適用することが可能です!
リフレクションはMessage
インターフェースの一部として提供されます。