HN 日本語サマリー

← 一覧へ戻る
プログラミング

Protobuf-py: 妥協なきPython用Protobuf

Protobuf-py: Protobuf for Python, without compromises (buf.build)

114 pointsby ming1329 コメント

要約

Bufは、Pythonのためにゼロから完全に書き直された新しいProtocol Buffersライブラリ「protobuf-py」を発表しました。このライブラリは、既存のPython Protobuf実装の欠点を克服し、完全な仕様カバレッジ、読みやすい生成コード、そして高いパフォーマンスを両立させています。Rustアクセラレータを併用することで、本番環境のワークロードにおいてGoogleのupb(Cエンジン)と同等の速度を実現します。

全文翻訳

本日、私たちはprotobuf-pyを発表します。これは、Pythonのためにゼロから完全に書き直されたProtocol Buffersライブラリです。proto2、proto3、およびエディションすべてにわたるProtobuf適合性スイートのバイナリおよびJSONの全ケースに合格し、拡張機能、カスタムオプション、不明なフィールド、動的メッセージ、およびよく知られた型をサポートします。読みやすく型付けされたPythonを生成し、ランタイム依存関係がなく、純粋なPython 3.10+で動作します。Rustアクセラレータをインストールすると、GoogleのPythonパッケージが動作するCエンジンであるupbと同様に、本番ワークロードでは同等の速度を発揮します。Python開発者にとって、歴史的に完全なProtobuf実装か、Pythonらしい使い心地のライブラリかの選択肢しかありませんでした。Googleのパッケージは完全ですが、APIはC++とJavaに影響された形です。betterprotoは使いやすいですが、仕様の多くを犠牲にしていました。protobuf-pyは、Python開発者に両方を提供します。 なぜ別のProtobufランタイムを構築するのか? PythonはProtobufにとって、後回しにされるにはあまりにも重要です。データパイプライン、MLシステム、AIエージェント、インフラスクリプト、RPCサービス、開発者ツールなどに使用されています。それにもかかわらず、PythonでProtobufを使用する体験は、この人気のある言語のデベロッパーが期待するものとは一致していません。Googleのパッケージは完全で実績がありますが、APIと生成コードは依然として他者のランタイムのバインディングのように感じられます。betterprotoはPython開発者がより良いものを求めていることを証明しましたが、仕様全体を実装することはありませんでした。grpcioは同じ問題をRPCにもたらしました。強力で広く使われていますが、その上で構築するのは困難です。当初、私たちはconnect-py、つまりConnectとgRPCの両方を話せるConnectRPC実装でRPCレイヤーを修正することを目指していました。しかし、トランスポートレイヤーだけでは不十分でした。良いRPCスタックは、その下のメッセージに依存しており、Pythonには基盤として望むProtobufランタイムがありませんでした。私たちは、実際のスキーマに対して十分な完全性、日常的なPythonで十分な読みやすさ、そして誰もが選択を後悔しないだけの速度を持つものを求めていました。protobuf-pyは、これらの制約がすべて同時に成り立つかどうかを問うことから生まれた結果です。Pythonのために構築された完全な実装であり、Googleのランタイムをラップしたり仕様を切り詰めたりすることなく、connect-pyが基盤とするのに望ましい仕様カバレッジ、生成コード、およびパフォーマンスプロファイルを提供します。 なぜGoogleのパッケージはそのように感じるのか PyPIからprotobufをインストールすると、通常得られるエンジンはCで書かれたupbです。あなたのメッセージはCのアリーナに存在し、Pythonオブジェクトはそのアリーナへのハンドルとなります。フィールドの読み取りはCに移行し、値を見つけ、戻る際にPythonオブジェクトを生成します。これは複数の言語間でエンジンを共有する良い方法ですが、あらゆる場所に痕跡を残します:生成される_pb2.pyファイルは、読み取るPythonコードが少ないため読みにくいです。クラスはインポート時にCエンジンの設定のために組み立てられるため、go-to-definitionはシリアライズされたディスクリプタバイトの壁に当たります。SerializeToString、HasField、WhichOneof、CopyFromは、C++の人間工学に基づいて設計されたPython APIを構成します。HasFieldはproto3のスカラーに対して呼び出すと例外が発生します。WhichOneofは、getattrに渡して既に配置された値を取得するための文字列を返します。生成されたインポートは絶対パスであり、パッケージ内にネストするとすぐに壊れます。PyPIに存在するfix-protobuf-importsという別のツールは、Googleの出力を書き換えるためだけに存在します。型はプロセス全体で1つのプールに登録されるため、同じ.protoの2つのビルドをインポートすると実行時に例外が発生します。これらはバインディングにおけるランダムな欠陥ではありません。これらは、主にC++/Javaのコードベースで一貫性を保つためにPython APIが設計された場合に起こることです。Pythonのイディオムに沿ったパッケージとしてではありません。不格好なAPIと速度はセットで提供され、長年、両方を取るか取らないかの選択でした。 代わりに構築したもの protobuf-pyは、メッセージをPython内に保持します。これは__slots__を持つプレーンなオブジェクトであり、そのフィールドは整数、文字列、リスト、サブメッセージなどの通常のPython値です。Rustアクセラレータは、必要な操作(主にパースとシリアライズ)を高速化し、結果を直接オブジェクトに書き込みます。パースが完了すると、フィールドの読み取りはPython属性へのアクセスと同じになります。データがPythonであるため、生成コードは実際のコードです。protoc-gen-pyは、あなたが読めるクラスを出力します: ```python class User(Message[_UserFields]): __slots__ = ("first_name", "last_name", "active", "manager", "locations", "projects", "contact") if TYPE_CHECKING: def __init__( self, *, first_name: str = "", last_name: str = "", active: bool = False, manager: User | None = None, locations: list[str] | None = None, projects: dict[str, str] | None = None, contact: Oneof[Literal["email"], str] | Oneof[["phone"], str] | None = None, ) -> None: ... first_name: str last_name: str active: bool manager: User | None locations: list[str] projects: dict[str, str] contact: Oneof[["email"], str] | Oneof[["phone"], str] | None ``` 操作は、言語内の他のものと同様に行えます: ```python import copy from gen.user_pb import User from protobuf import Oneof user = User(first_name="Alice", active=True, locations=["NYC", "LDN"]) user.last_name = "Smith" wire = user.to_binary() parsed = User.from_binary(wire) match parsed.contact: case Oneof(field="email", value=email): send(email) case Oneof(field="phone", value=phone): call(phone) inactive = copy.replace(parsed, active=False) # Python 3.13+ ``` Oneofはパターンマッチング可能な値になり、型チェッカーは各ブランチを絞り込みます。Enumは実際のIntEnumメンバーです。pyright、mypy、tyは、スタブパッケージなしで生成された出力を読み取ります。生成されたファイルは相対インポートを使用し、どこに配置しても機能します。型はグローバルプールではなく、明示的なRegistryを通じて解決されます。これらすべては、メッセージデータをPython内に保持することから生まれます。 完全性、フレンドリーなサブセットではない 他のProtobufライブラリは、Googleのものよりも使いやすいですが、通常はそれを得るために仕様の半分を落としています。betterprotoは現在最も使いやすいオプションですが、proto3のみで、proto2、エディション、拡張機能、カスタムオプションはありません。protobuf-pyは仕様全体をカバーします。proto2、proto3、エディション、拡張機能、カスタムオプション、グループ、ラウンドトリップをまたぐ不明フィールドの保持、パックおよび展開された繰り返しフィールド、そしてよく知られた型の完全なProtoJSONエンコーディングを処理します。Googleが独自のランタイムを資格認定するために使用する適合性スイートに合格し、バイナリとJSON全体で失敗はありません。リポジトリには空の失敗リストがチェックインされており、それが空でなくなるとCIは失敗します。 重要な部分での高速性 メッセージをパースして破棄するベンチマークでは、作業を読み取りまで遅延させるため、upbは無敵に見えます。本番コードはメッセージを一度パースし、フィールドで分岐し、いくつかの値を取り出し、メッセージをコピーし、変更されたバージョンをシリアライズして戻します。upbは、これらの読み取りごとにPythonへの変換コストを支払います。protobuf-pyは、最初の読み取り後、そのコストをゼロ回支払います。なぜなら、パースは既に通常のPythonオブジェクトを生成しているからです。コストは境界で逆方向に走ります。データをPython内に保持することは、protobuf-pyがより多くの作業を前もって行うことを意味します。これは、コードがメッセージに対して十分な作業を行い、時間を回収できる場合に効果を発揮します。ここでは、ソーシャルメディアサイトのユーザーのホームページへの応答を構築する実際の例と、それらの生のマーシャリングステップを両方のパッケージで比較します。数値は、upbに対するスループット(操作/秒)の相対値であり、高いほど高速です。 ワークロード | upb | protobuf-py ---|---|--- ホーム​​ページ応答の構築(エンドツーエンド) | 1.0x | 1.06x └ 単独でのパース | 1.0x | 0.22x └ 単独でのシリアライズ | 1.0x | 0.60x 単独では、upbがマーシャリングで勝利します。これは予想通りです。エンドツーエンドでは、サービスが本番で実際に実行するコードでは、protobuf-pyが優位に立ちます。upbがPythonに翻訳し直す必要がある各フィールドの読み取りは、protobuf-pyが既に持っているものであり、完全なリクエスト全体で、Cのものよりも高速なランタイムにつながります。ここのペイロードはテキスト中心(完全なReddit投稿本文、複数文のバイオグラフィーや通知)であり、ソーシャル、ドキュメント、LLM/エージェントトラフィックの現実的な形状です。これは、