Protocol Buffer の基本: Python

Protocol Buffer を使用するための基本的な Python プログラマー向け入門

このチュートリアルでは、Protocol Buffer を使用するための基本的な Python プログラマー向け入門を提供します。簡単なサンプル アプリケーションの作成を通して、以下の方法を説明します。

  • .proto ファイルでメッセージ形式を定義する。
  • Protocol Buffer コンパイラを使用する。
  • Python Protocol Buffer API を使用してメッセージを書き込みおよび読み取りする。

これは、Python での Protocol Buffer の使用に関する包括的なガイドではありません。詳細なリファレンス情報については、Protocol Buffer 言語ガイド (proto2)Protocol Buffer 言語ガイド (proto3)Python API リファレンスPython 生成コード ガイド、および エンコード リファレンス を参照してください。

問題領域

ここで使用する例は、人々の連絡先の詳細をファイル間で読み書きできる非常に簡単な「アドレス帳」アプリケーションです。アドレス帳の各個人には、名前、ID、メールアドレス、および連絡先の電話番号があります。

このような構造化データをシリアライズおよび取得するにはどうすればよいでしょうか?この問題を解決するには、いくつかの方法があります。

  • Python の pickle 化を使用する。これは言語に組み込まれているためデフォルトのアプローチですが、スキーマの進化にはうまく対応できず、C++ または Java で記述されたアプリケーションとデータを共有する必要がある場合にはあまりうまく機能しません。
  • データ項目を単一の文字列にエンコードするアドホックな方法を考案できます。たとえば、4 つの整数を「12:3:-23:67」としてエンコードするなどです。これはシンプルで柔軟なアプローチですが、一度限りのエンコードおよび解析コードを作成する必要があり、解析にはわずかなランタイム コストがかかります。これは非常に単純なデータをエンコードするのに最適です。
  • データを XML にシリアライズする。XML は (ある意味) 人間が読める形式であり、多くの言語用のバインディング ライブラリがあるため、このアプローチは非常に魅力的です。他のアプリケーション/プロジェクトとデータを共有したい場合に適しています。ただし、XML は非常に容量を消費し、エンコード/デコードするとアプリケーションに大きなパフォーマンス上のペナルティが生じる可能性があります。また、XML DOM ツリーのナビゲーションは、通常クラス内の単純なフィールドをナビゲートするよりもかなり複雑です。

これらのオプションの代わりに、Protocol Buffer を使用できます。Protocol Buffer は、まさにこの問題を解決するための、柔軟で効率的な自動化されたソリューションです。Protocol Buffer を使用すると、保存したいデータ構造の .proto 記述を記述します。それから、Protocol Buffer コンパイラは、効率的なバイナリ形式で Protocol Buffer データの自動エンコードと解析を実装するクラスを作成します。生成されたクラスは、Protocol Buffer を構成するフィールドのゲッターとセッターを提供し、Protocol Buffer をユニットとして読み書きする詳細を処理します。重要なことは、Protocol Buffer 形式が、古い形式でエンコードされたデータをコードが引き続き読み取れるように、形式を時間の経過とともに拡張するという考え方をサポートしていることです。

サンプル コードの場所

サンプル コードは、ソース コード パッケージの「examples」ディレクトリに含まれています。こちらからダウンロードしてください。

プロトコル形式の定義

アドレス帳アプリケーションを作成するには、まず .proto ファイルから始める必要があります。.proto ファイルの定義は簡単です。シリアライズしたいデータ構造ごとにメッセージを追加し、メッセージ内の各フィールドの名前と型を指定します。メッセージを定義する .proto ファイル addressbook.proto を次に示します。

syntax = "proto2";

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;
}

message AddressBook {
  repeated Person people = 1;
}

ご覧のとおり、構文は C++ または Java に似ています。ファイルの各部分を見て、何をするのかを確認しましょう。

.proto ファイルは、パッケージ宣言で始まります。これは、異なるプロジェクト間での名前の衝突を防ぐのに役立ちます。Python では、パッケージは通常ディレクトリ構造によって決定されるため、.proto ファイルで定義する package は、生成されたコードに影響を与えません。ただし、Protocol Buffer 名前空間および非 Python 言語での名前の衝突を避けるために、宣言する必要があります。

次に、メッセージ定義があります。メッセージは、型付きフィールドのセットを含む集約です。boolint32floatdoublestring など、多くの標準的な単純なデータ型がフィールド型として利用できます。また、他のメッセージ型をフィールド型として使用することで、メッセージにさらに構造を追加することもできます。上記の例では、Person メッセージに PhoneNumber メッセージが含まれ、AddressBook メッセージに Person メッセージが含まれています。メッセージ型を他のメッセージ内にネストして定義することもできます。ご覧のとおり、PhoneNumber 型は Person 内で定義されています。フィールドの 1 つに定義済み値のリストの 1 つを持たせたい場合は、enum 型を定義することもできます。ここでは、電話番号を次の電話の種類 (PHONE_TYPE_MOBILEPHONE_TYPE_HOME、または PHONE_TYPE_WORK) のいずれかに指定したいとします。

各要素の「 = 1」、「 = 2」マーカーは、バイナリ エンコードでフィールドが使用する一意の「タグ」を識別します。タグ番号 1~15 は、それより大きい番号よりもエンコードに必要なバイト数が 1 バイト少なくなります。そのため、最適化として、これらのタグを一般的に使用される要素または繰り返し要素に使用し、タグ 16 以降をあまり使用されないオプション要素に残すことができます。繰り返しフィールドの各要素はタグ番号を再エンコードする必要があるため、繰り返しフィールドはこの最適化に特に適しています。

各フィールドには、次の修飾子のいずれかを注釈する必要があります。

  • optional: フィールドは設定されている場合とされていない場合があります。オプションのフィールド値が設定されていない場合、デフォルト値が使用されます。単純な型の場合、例の電話番号の type で行ったように、独自のデフォルト値を指定できます。それ以外の場合は、システム デフォルトが使用されます。数値型の場合はゼロ、文字列の場合は空文字列、ブール値の場合は false です。埋め込みメッセージの場合、デフォルト値は常にメッセージの「デフォルト インスタンス」または「プロトタイプ」であり、フィールドは設定されていません。明示的に設定されていないオプション (または必須) フィールドの値を取得するためにアクセサーを呼び出すと、常にそのフィールドのデフォルト値が返されます。
  • repeated: フィールドは任意の回数 (ゼロを含む) 繰り返すことができます。繰り返し値の順序は、Protocol Buffer で保持されます。繰り返しフィールドは、動的にサイズ変更される配列と考えてください。
  • required: フィールドの値を提供する必要があります。そうしないと、メッセージは「初期化されていない」と見なされます。初期化されていないメッセージをシリアライズすると、例外が発生します。初期化されていないメッセージを解析すると失敗します。これ以外は、必須フィールドはオプション フィールドとまったく同じように動作します。

可能なすべてのフィールド型を含む、.proto ファイルの書き方に関する完全なガイドは、Protocol Buffer 言語ガイド にあります。クラスの継承に似た機能を探さないでください。Protocol Buffer はそれを行いません。

Protocol Buffer のコンパイル

.proto ができたので、次に必要なことは、AddressBook (したがって PersonPhoneNumber) メッセージを読み書きするために必要なクラスを生成することです。これを行うには、Protocol Buffer コンパイラ protoc.proto で実行する必要があります。

  1. コンパイラをインストールしていない場合は、パッケージをダウンロード し、README の指示に従ってください。

  2. 次に、コンパイラを実行して、ソース ディレクトリ (アプリケーションのソース コードが存在する場所。値を指定しない場合は現在のディレクトリが使用されます)、宛先ディレクトリ (生成されたコードを配置する場所。多くの場合、$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) が生成されます。

Protocol Buffer API

Java および C++ Protocol Buffer コードを生成する場合とは異なり、Python Protocol Buffer コンパイラは、データ アクセス コードを直接生成しません。代わりに (addressbook_pb2.py を見るとわかるように)、すべてのメッセージ、enum、およびフィールドの特別な記述子と、各メッセージ タイプに対して 1 つの不思議な空のクラスを生成します。

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 生成コード リファレンス を参照してください。

Enum

Enum は、メタクラスによって整数値を持つシンボリック定数のセットに展開されます。したがって、たとえば、定数 addressbook_pb2.Person.PhoneType.PHONE_TYPE_WORK の値は 2 です。

標準メッセージ メソッド

各メッセージ クラスには、メッセージ全体を確認または操作できる他の多くのメソッドも含まれています。以下を含みます。

  • IsInitialized(): 必須フィールドがすべて設定されているかどうかを確認します。
  • __str__(): メッセージの人間が読める表現を返します。特にデバッグに役立ちます。(通常は str(message) または print message として呼び出されます。)
  • CopyFrom(other_msg): 指定されたメッセージの値でメッセージを上書きします。
  • Clear(): すべての要素を空の状態に戻します。

これらのメソッドは、Message インターフェイスを実装します。詳細については、Message の完全な API ドキュメント を参照してください。

解析とシリアライゼーション

最後に、各 Protocol Buffer クラスには、Protocol Buffer バイナリ形式 を使用して、選択した型のメッセージを書き込みおよび読み取りするためのメソッドがあります。これらには以下が含まれます。

  • SerializeToString(): メッセージをシリアライズし、文字列として返します。バイトはバイナリであり、テキストではないことに注意してください。str 型は便利なコンテナとしてのみ使用します。
  • ParseFromString(data): 指定された文字列からメッセージを解析します。

これらは、解析とシリアライゼーション用に提供されるオプションのほんの一部です。繰り返しますが、完全なリストについては、Message API リファレンス を参照してください。

メッセージの書き込み

それでは、Protocol Buffer クラスを使ってみましょう。アドレス帳アプリケーションで最初にできるようにしたいことは、個人情報をアドレス帳ファイルに書き込むことです。これを行うには、Protocol Buffer クラスのインスタンスを作成してデータを入力し、それらを出力ストリームに書き込む必要があります。

これは、ファイルから AddressBook を読み取り、ユーザー入力に基づいて新しい Person を 1 人追加し、新しい AddressBook をファイルに書き戻すプログラムです。Protocol コンパイラによって生成されたコードを直接呼び出すか参照する部分は強調表示されています。

#!/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)

Protocol Buffer の拡張

Protocol Buffer を使用するコードをリリースした後、遅かれ早かれ、Protocol Buffer の定義を「改善」したくなるでしょう。新しいバッファーに下位互換性を持たせ、古いバッファーに上位互換性を持たせたい場合 (そして、ほぼ確実にそうしたいはずです)、従う必要のあるいくつかのルールがあります。Protocol Buffer の新しいバージョンでは

  • 既存のフィールドのタグ番号を変更してはなりません
  • 必須フィールドを追加または削除してはなりません
  • オプション フィールドまたは繰り返しフィールドを削除してもかまいません
  • 新しいオプション フィールドまたは繰り返しフィールドを追加してもかまいませんが、新しいタグ番号 (つまり、この Protocol Buffer でこれまで使用されたことのないタグ番号。削除されたフィールドで使用されたタグ番号も含む) を使用する必要があります。

(これらのルールには いくつかの例外 がありますが、めったに使用されません。)

これらのルールに従うと、古いコードは新しいメッセージを問題なく読み取り、新しいフィールドを単に無視します。古いコードの場合、削除されたオプション フィールドは単にデフォルト値を持ち、削除された繰り返しフィールドは空になります。新しいコードも古いメッセージを透過的に読み取ります。ただし、新しいオプション フィールドは古いメッセージには存在しないことに注意してください。そのため、has_ で設定されているかどうかを明示的に確認するか、タグ番号の後に [default = value] を使用して .proto ファイルで適切なデフォルト値を指定する必要があります。オプション要素にデフォルト値が指定されていない場合、代わりに型固有のデフォルト値が使用されます。文字列の場合、デフォルト値は空文字列です。ブール値の場合、デフォルト値は false です。数値型の場合、デフォルト値はゼロです。また、新しい繰り返しフィールドを追加した場合、新しいコードでは、それが (新しいコードによって) 空のままにされたのか、(古いコードによって) まったく設定されなかったのかを区別できません。繰り返しフィールドには has_ フラグがないためです。

高度な使用法

Protocol Buffer には、単純なアクセサーやシリアライゼーションを超えた用途があります。Python API リファレンス を調べて、他に何ができるかを確認してください。

プロトコル メッセージ クラスによって提供される主要な機能の 1 つは、リフレクションです。メッセージのフィールドを反復処理し、特定のメッセージ タイプに対してコードを記述せずに値を操作できます。リフレクションを使用する非常に便利な方法の 1 つは、プロトコル メッセージを XML や JSON などの他のエンコードとの間で変換することです。リフレクションのより高度な使用法は、同じ型の 2 つのメッセージ間の差異を見つけたり、「プロトコル メッセージの正規表現」のようなものを開発して、特定のメッセージの内容に一致する式を記述したりすることです。想像力を働かせれば、Protocol Buffer を最初に予想していたよりもはるかに広範囲の問題に適用できる可能性があります。

リフレクションは、Message インターフェイス の一部として提供されています。