これは、なにをしたくて書いたもの?
PythonでProtocol Buffersを扱ってみようかな、ということで。
Javaでなら以前扱ったことがあります。とはいえ、久しぶりに触るので情報も見返してみましょう。
Protocol Buffers
Protocol BuffersのWebサイトはこちら。
Protocol Buffers Documentation
GitHub - protocolbuffers/protobuf: Protocol Buffers - Google's data interchange format
Protocol Buffersは、言語およびプラットフォームに非依存の構造化データをシリアライズするメカニズムです。
Protocol Buffers are a language-neutral, platform-neutral extensible mechanism for serializing structured data.
C++やC#、Java、Goなど様々なプログラミング言語で生成されたコードをサポートしています。
特徴は以下のとおりです。
Overview | Protocol Buffers Documentation
プログラミングガイドはこちら。
Programming Guides | Protocol Buffers Documentation
よく見ると、現在のProtocol Buffersには以下の種類があるようです。
- proto 2
- proto 3
- Editions
こちらが言語リファレンスで、各言語向けにコードを生成した場合の型のマッピングなども書かれていますね。
Editionsには2023、そして開発中の2024があります。
Editionsとproto 2、proto 3との違いはこちらを見るとよさそうです。
Protobuf Editions Overview | Protocol Buffers Documentation
現時点で、大きくproto 3と違うわけではなさそうですが。
今回は、proto 3を扱うことにします。
Language Guide (proto 3) | Protocol Buffers Documentation
言語はPythonを使います。
Python Reference | Protocol Buffers Documentation
チュートリアルはこちら。
Protocol Buffer Basics: Python | Protocol Buffers Documentation
今回は簡単な例で試してみることにします。
環境
今回の環境はこちら。
$ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 24.04.3 LTS Release: 24.04 Codename: noble $ uname -srvmpio Linux 6.8.0-71-generic #71-Ubuntu SMP PREEMPT_DYNAMIC Tue Jul 22 16:52:38 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux $ python3 --version Python 3.12.3 $ uv --version uv 0.8.19
Protocol Buffersコンパイラー(Protocol Buffer Compiler、protoc)をインストールする
まずはProtocol Buffersのコンパイラー、Protocol Buffer Compiler(protoc)をインストールします。
Protocol Buffer Compiler Installation | Protocol Buffers Documentation
コンパイル済みのバイナリーを使うか、OSのパッケージマネージャーを使うかしてインストールします。
Ubuntu Linux 24.04 LTSではaptでインストールできるバージョンが最新版と同じようなので、aptでインストールすることにします。
$ apt show protobuf-compiler Package: protobuf-compiler Version: 3.21.12-8.2ubuntu0.2 Priority: extra Section: universe/devel Source: protobuf Origin: Ubuntu Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com> Original-Maintainer: Laszlo Boszormenyi (GCS) <gcs@debian.org> Bugs: https://bugs.launchpad.net/ubuntu/+filebug Installed-Size: 116 kB Depends: libc6 (>= 2.34), libgcc-s1 (>= 3.3.1), libprotoc32t64 (= 3.21.12-8.2ubuntu0.2), libstdc++6 (>= 13.1) Recommends: libprotobuf-dev Suggests: protobuf-mode-el Homepage: https://github.com/google/protobuf/ Download-Size: 29.0 kB APT-Sources: http://archive.ubuntu.com/ubuntu noble-updates/universe amd64 Packages Description: compiler for protocol buffer definition files Protocol buffers are a flexible, efficient, automated mechanism for serializing structured data - similar to XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages. You can even update your data structure without breaking deployed programs that are compiled against the "old" format. . Google uses Protocol Buffers for almost all of its internal RPC protocols and file formats. . This package contains the protocol buffer compiler that is used for translating from .proto files (containing the definitions) to the language binding for the supported languages. N: 追加レコードが 1 件あります。表示するには '-a' スイッチを付けてください。
Ubuntu Manpage: protoc - compile protocol buffer description files
インストール。
$ sudo apt install protobuf-compiler
バージョン。
$ protoc --version libprotoc 3.21.12
これでprotocを使う準備はできました。
uvプロジェクトを作成して、Protocol Buffersの定義からソースコードを生成する
uvプロジェクトを作成します。
$ uv init --vcs none protobuf-getting-started $ cd protobuf-getting-started $ rm main.py
依存関係をいろいろとインストールする前に、ソースコードを生成してしまいましょう。
こちらを見ながら進めていきます。
Protocol Buffer Basics: Python | Protocol Buffers Documentation
出力先はsrc/genとします。
$ mkdir -p src/gen
定義はこのようなものにしました。書籍がお題ですね。
book.proto
syntax="proto3"; package tutorial; message Book { string isbn13 = 1; string title = 2; repeated Author authors = 3; int32 price = 4; enum Tag { BEGGINER = 0; ADVANCED = 1; PYTHON = 2; JAVA = 3; MYSQL = 4; } repeated Tag tags = 5; } message Author { string first_name = 1; string last_name = 2; }
ソースコードを生成。--proto_pathは、Protocol Buffersの定義ファイルがどこにあるかを指定します。--python_outは生成する
Pythonコードの出力先、--pyi_outはスタブファイル(型定義)の出力先です。
$ protoc --proto_path=. --python_out=src/gen --pyi_out=src/gen book.proto
結果はこうなりました。
src/gen/book_pb2.py
# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: book.proto """Generated protocol buffer code.""" from google.protobuf.internal import builder as _builder 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 # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nbook.proto\x12\x08tutorial\"\xbd\x01\n\x04\x42ook\x12\x0e\n\x06isbn13\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12!\n\x07\x61uthors\x18\x03 \x03(\x0b\x32\x10.tutorial.Author\x12\r\n\x05price\x18\x04 \x01(\x05\x12 \n\x04tags\x18\x05 \x03(\x0e\x32\x12.tutorial.Book.Tag\"B\n\x03Tag\x12\x0c\n\x08\x42\x45GGINER\x10\x00\x12\x0c\n\x08\x41\x44VANCED\x10\x01\x12\n\n\x06PYTHON\x10\x02\x12\x08\n\x04JAVA\x10\x03\x12\t\n\x05MYSQL\x10\x04\"/\n\x06\x41uthor\x12\x12\n\nfirst_name\x18\x01 \x01(\t\x12\x11\n\tlast_name\x18\x02 \x01(\tb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'book_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _BOOK._serialized_start=25 _BOOK._serialized_end=214 _BOOK_TAG._serialized_start=148 _BOOK_TAG._serialized_end=214 _AUTHOR._serialized_start=216 _AUTHOR._serialized_end=263 # @@protoc_insertion_point(module_scope)
スタブファイル。
src/gen/book_pb2.pyi
from google.protobuf.internal import containers as _containers from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor class Author(_message.Message): __slots__ = ["first_name", "last_name"] FIRST_NAME_FIELD_NUMBER: _ClassVar[int] LAST_NAME_FIELD_NUMBER: _ClassVar[int] first_name: str last_name: str def __init__(self, first_name: _Optional[str] = ..., last_name: _Optional[str] = ...) -> None: ... class Book(_message.Message): __slots__ = ["authors", "isbn13", "price", "tags", "title"] class Tag(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = [] ADVANCED: Book.Tag AUTHORS_FIELD_NUMBER: _ClassVar[int] BEGGINER: Book.Tag ISBN13_FIELD_NUMBER: _ClassVar[int] JAVA: Book.Tag MYSQL: Book.Tag PRICE_FIELD_NUMBER: _ClassVar[int] PYTHON: Book.Tag TAGS_FIELD_NUMBER: _ClassVar[int] TITLE_FIELD_NUMBER: _ClassVar[int] authors: _containers.RepeatedCompositeFieldContainer[Author] isbn13: str price: int tags: _containers.RepeatedScalarFieldContainer[Book.Tag] title: str def __init__(self, isbn13: _Optional[str] = ..., title: _Optional[str] = ..., authors: _Optional[_Iterable[_Union[Author, _Mapping]]] = ..., price: _Optional[int] = ..., tags: _Optional[_Iterable[_Union[Book.Tag, str]]] = ...) -> None: ...
ちなみにスタブファイルは、mypy向けに生成することもできます。こちらは後で置き換えます。
では、生成されたコードを使っていきます。
準備
必要な依存関係をインストールします。
Protocol Buffers。
$ uv add protobuf
スタブファイルもインストール。
$ uv add --dev types-protobuf
テストや各種チェックツールも入れておきます。
$ uv add --dev pytest mypy ruff
今回mypyを使うのですが、protocが生成するファイルでは型情報が弱く、mypyでのチェックを通すことができません。
そこで今回はmypy-protobufを使います。
$ uv tool install mypy-protobuf
GitHub - nipunn1313/mypy-protobuf: open source tools to generate mypy stubs from protobufs
こちらをインストールするとprotoc-gen-mypyというコマンドが使えるようになります。
$ which protoc-gen-mypy $HOME/.local/bin/protoc-gen-mypy $ protoc-gen-mypy --version mypy-protobuf 3.6.0
この状態だと、protocコマンドに--mypy_outというオプションが指定できるようになり、型情報が強化されたスタブファイルを生成できます。
$ protoc --proto_path=. --python_out=src/gen --mypy_out=src/gen book.proto
結果はこうなりました。
src/gen/book_pb2.pyi
""" @generated by mypy-protobuf. Do not edit manually! isort:skip_file """ import builtins import collections.abc import google.protobuf.descriptor import google.protobuf.internal.containers import google.protobuf.internal.enum_type_wrapper import google.protobuf.message import sys import typing if sys.version_info >= (3, 10): import typing as typing_extensions else: import typing_extensions DESCRIPTOR: google.protobuf.descriptor.FileDescriptor @typing.final class Book(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor class _Tag: ValueType = typing.NewType("ValueType", builtins.int) V: typing_extensions.TypeAlias = ValueType class _TagEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Book._Tag.ValueType], builtins.type): DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor BEGGINER: Book._Tag.ValueType # 0 ADVANCED: Book._Tag.ValueType # 1 PYTHON: Book._Tag.ValueType # 2 JAVA: Book._Tag.ValueType # 3 MYSQL: Book._Tag.ValueType # 4 class Tag(_Tag, metaclass=_TagEnumTypeWrapper): ... BEGGINER: Book.Tag.ValueType # 0 ADVANCED: Book.Tag.ValueType # 1 PYTHON: Book.Tag.ValueType # 2 JAVA: Book.Tag.ValueType # 3 MYSQL: Book.Tag.ValueType # 4 ISBN13_FIELD_NUMBER: builtins.int TITLE_FIELD_NUMBER: builtins.int AUTHORS_FIELD_NUMBER: builtins.int PRICE_FIELD_NUMBER: builtins.int TAGS_FIELD_NUMBER: builtins.int isbn13: builtins.str title: builtins.str price: builtins.int @property def authors(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Author]: ... @property def tags(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___Book.Tag.ValueType]: ... def __init__( self, *, isbn13: builtins.str = ..., title: builtins.str = ..., authors: collections.abc.Iterable[global___Author] | None = ..., price: builtins.int = ..., tags: collections.abc.Iterable[global___Book.Tag.ValueType] | None = ..., ) -> None: ... def ClearField(self, field_name: typing.Literal["authors", b"authors", "isbn13", b"isbn13", "price", b"price", "tags", b"tags", "title", b"title"]) -> None: ... global___Book = Book @typing.final class Author(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor FIRST_NAME_FIELD_NUMBER: builtins.int LAST_NAME_FIELD_NUMBER: builtins.int first_name: builtins.str last_name: builtins.str def __init__( self, *, first_name: builtins.str = ..., last_name: builtins.str = ..., ) -> None: ... def ClearField(self, field_name: typing.Literal["first_name", b"first_name", "last_name", b"last_name"]) -> None: ... global___Author = Author
コメントをよく見ると、mypy-protobufによって生成されたことが書かれています。
""" @generated by mypy-protobuf. Do not edit manually! isort:skip_file """
ここまででインストールした依存関係はこちら。
$ uv pip list Package Version ----------------- --------------- iniconfig 2.1.0 mypy 1.18.2 mypy-extensions 1.1.0 packaging 25.0 pathspec 0.12.1 pluggy 1.6.0 protobuf 6.32.1 pygments 2.19.2 pytest 8.4.2 ruff 0.13.1 types-protobuf 6.32.1.20250918 typing-extensions 4.15.0
pyproject.toml
[project]
name = "protobuf-getting-started"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"protobuf>=6.32.1",
]
[dependency-groups]
dev = [
"mypy>=1.18.2",
"pytest>=8.4.2",
"ruff>=0.13.1",
"types-protobuf>=6.32.1.20250918",
]
[tool.ruff]
extend-exclude = ["src/gen"]
[tool.mypy]
strict = true
disallow_any_unimported = true
disallow_any_expr = true
disallow_any_explicit = true
warn_unreachable = true
pretty = true
explicit_package_bases = true
mypy_path = ["src"]
[tool.pytest.ini_options]
pythonpath = ["src"]
生成されたコードを使ってみる
それでは、生成されたコードを使ってみます。
参考にするのは、このあたりです。
Protocol Buffer Basics: Python | Protocol Buffers Documentation
Python Generated Code Guide | Protocol Buffers Documentation
この2つを見ると、だいたい使い方はわかるような気がします。
APIリファレンスはこちらですね。
Protocol Buffers Python API Reference — Protocol Buffers 4.21.1 documentation
オブジェクトの作成、JSONでのシリアライズ、Protocol Buffersとしてのシリアライズをそれぞれ行っています。
rc/main.py
from google.protobuf import json_format from gen import book_pb2 def main() -> None: python_book = book_pb2.Book() python_book.isbn13 = "978-4873119328" python_book.title = "入門 Python 3 第2版" python_book.price = 4180 python_book.tags.append(book_pb2.Book.BEGGINER) python_book.tags.append(book_pb2.Book.Tag.PYTHON) python_book_author1 = book_pb2.Author() python_book_author1.first_name = "Bill" python_book_author1.last_name = "Lubanovic" python_book.authors.append(python_book_author1) python_book_author2 = python_book.authors.add() python_book_author2.first_name = "駿 " python_book_author2.last_name = "鈴木" python_book_author3 = python_book.authors.add() python_book_author3.first_name = "高弘" python_book_author3.last_name = "長尾" print("==================== Python Book ====================") print("Plain") print(python_book) print() print("JSON:") print(json_format.MessageToJson(python_book)) print() print("Serialize:") print(python_book.SerializeToString()) print() java_book = book_pb2.Book() java_book.isbn13 = "978-4621303252" java_book.title = "Effective Java 第3版" java_book.price = 4400 java_book.tags.append(book_pb2.Book.Tag.Value("ADVANCED")) java_book.tags.append(book_pb2.Book.Tag.Value("JAVA")) java_book_author1 = book_pb2.Author() java_book_author1.first_name = "Joshua" java_book_author1.last_name = "Bloch" java_book.authors.append(java_book_author1) java_book_author1 = book_pb2.Author() java_book_author1.first_name = "Joshua" java_book_author1.last_name = "Bloch" java_book.authors.append(java_book_author1) print("==================== Java Book ====================") print("Plain") print(java_book) print() print("JSON:") print(json_format.MessageToJson(java_book)) print() print("Serialize:") print(java_book.SerializeToString()) if __name__ == "__main__": main()
リストへの追加方法は2種類あったりします。
python_book_author1 = book_pb2.Author()
python_book_author1.first_name = "Bill"
python_book_author1.last_name = "Lubanovic"
python_book.authors.append(python_book_author1)
python_book_author2 = python_book.authors.add()
python_book_author2.first_name = "駿 "
python_book_author2.last_name = "鈴木"
python_book_author3 = python_book.authors.add()
python_book_author3.first_name = "高弘"
python_book_author3.last_name = "長尾"
あと、Enumの扱いが最初わかりませんでした…。
シリアライズ。JSONとProtocol Buffersでのシリアライズ。順番が逆な気がしますが。
print("JSON:") print(json_format.MessageToJson(java_book)) print() print("Serialize:") print(java_book.SerializeToString())
結果。
$ uv run src/main.py
==================== Python Book ====================
Plain
isbn13: "978-4873119328"
title: "入門 Python 3 第2版"
authors {
first_name: "Bill"
last_name: "Lubanovic"
}
authors {
first_name: "駿 "
last_name: "鈴木"
}
authors {
first_name: "高弘"
last_name: "長尾"
}
price: 4180
tags: BEGGINER
tags: PYTHON
JSON:
{
"isbn13": "978-4873119328",
"title": "\u5165\u9580 Python 3 \u7b2c2\u7248",
"authors": [
{
"firstName": "Bill",
"lastName": "Lubanovic"
},
{
"firstName": "\u99ff ",
"lastName": "\u9234\u6728"
},
{
"firstName": "\u9ad8\u5f18",
"lastName": "\u9577\u5c3e"
}
],
"price": 4180,
"tags": [
"BEGGINER",
"PYTHON"
]
}
Serialize:
b'\n\x0e978-4873119328\x12\x17\xe5\x85\xa5\xe9\x96\x80 Python 3 \xe7\xac\xac2\xe7\x89\x88\x1a\x11\n\x04Bill\x12\tLubanovic\x1a\x0e\n\x04\xe9\xa7\xbf \x12\x06\xe9\x88\xb4\xe6\x9c\xa8\x1a\x10\n\x06\xe9\xab\x98\xe5\xbc\x98\x12\x06\xe9\x95\xb7\xe5\xb0\xbe \xd4 *\x02\x00\x02'
==================== Java Book ====================
Plain
isbn13: "978-4621303252"
title: "Effective Java 第3版"
authors {
first_name: "Joshua"
last_name: "Bloch"
}
authors {
first_name: "芳樹"
last_name: "柴田"
}
price: 4400
tags: ADVANCED
tags: JAVA
JSON:
{
"isbn13": "978-4621303252",
"title": "Effective Java \u7b2c3\u7248",
"authors": [
{
"firstName": "Joshua",
"lastName": "Bloch"
},
{
"firstName": "\u82b3\u6a39",
"lastName": "\u67f4\u7530"
}
],
"price": 4400,
"tags": [
"ADVANCED",
"JAVA"
]
}
Serialize:
b'\n\x0e978-4621303252\x12\x16Effective Java \xe7\xac\xac3\xe7\x89\x88\x1a\x0f\n\x06Joshua\x12\x05Bloch\x1a\x10\n\x06\xe8\x8a\xb3\xe6\xa8\xb9\x12\x06\xe6\x9f\xb4\xe7\x94\xb0 \xb0"*\x02\x01\x03'
テストでは、シリアライズしてファイルに保存、ファイルからデシリアライズしてみることを確認。
tests/test_main.py
import os
from gen import book_pb2
def test_book_deser() -> None:
python_book = book_pb2.Book()
python_book.isbn13 = "978-4873119328"
python_book.title = "入門 Python 3 第2版"
python_book.price = 4180
python_book.tags.append(book_pb2.Book.BEGGINER)
python_book.tags.append(book_pb2.Book.PYTHON)
python_book_author1 = book_pb2.Author()
python_book_author1.first_name = "Bill"
python_book_author1.last_name = "Lubanovic"
python_book.authors.append(python_book_author1)
python_book_author2 = python_book.authors.add()
python_book_author2.first_name = "駿 "
python_book_author2.last_name = "鈴木"
python_book_author3 = python_book.authors.add()
python_book_author3.first_name = "高弘"
python_book_author3.last_name = "長尾"
with open("/tmp/book.pb", "wb") as f:
f.write(python_book.SerializeToString())
with open("/tmp/book.pb", "rb") as f:
restore_book = book_pb2.Book()
restore_book.ParseFromString(f.read())
assert restore_book.isbn13 == "978-4873119328"
assert restore_book.title == "入門 Python 3 第2版"
assert restore_book.price == 4180
assert len(restore_book.tags) == 2
assert restore_book.tags == [book_pb2.Book.BEGGINER, book_pb2.Book.PYTHON]
author1 = book_pb2.Author()
author1.first_name = "Bill"
author1.last_name = "Lubanovic"
author2 = book_pb2.Author()
author2.first_name = "駿 "
author2.last_name = "鈴木"
author3 = book_pb2.Author()
author3.first_name = "高弘"
author3.last_name = "長尾"
assert len(restore_book.authors) == 3
assert restore_book.authors == [
author1,
author2,
author3,
]
assert os.path.exists("/tmp/book.pb")
os.remove("/tmp/book.pb")
assert not os.path.exists("/tmp/book.pb")
こんなところでしょうか。
おわりに
PythonでProtocol Buffersを扱ってみました。
生成されたソースコードやProtocol BuffersのAPIに戸惑ったところもありましたが、それ以上にRuffやmypyとの組み合わせで困っていた時間の方が
長かった気がします。
このあたりのツールの設定に慣れていないだけですが。
ひとまず使い方の雰囲気はわかったのでよしとしましょう。