3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonにおけるgRPCライブラリの選定

Last updated at Posted at 2025-12-16

PythonアプリケーションにgRPCを組み込む際に、ライブラリの選定に悩んだのでその際に調査した情報を本記事で共有します。

注意 : 本記事の内容は所属企業の方針ではなく、筆者の見解です

gRPCについて

gRPCは、Googleが開発したオープンソースのRPC(Remote Procedure Call)フレームワークです。HTTP/2を通じて高速で効率的な通信を実現します。複数のプログラミング言語に対応しており、マイクロサービスアーキテクチャの構築に広く使用されています。詳細は本記事では割愛するので、gRPC公式サイトを参照してください。

gRPCをライブラリに組み込む場合のライブラリ/モジュール構成

gRPCを自身のPythonアプリケーションに組み込むケースを想定します。
サーバが提供するサービスをサービスAとします。これは*.protoファイルで定義されているとします。
サーバアプリ/クライアントアプリを実装する際に、gRPCに関するライブラリ/モジュールの構成は下図のようになります。

swstack.png

この時、(1)/(4)はユーザが実装します。(2)/(5)は生成ツールを使って生成します。(3)はOSSとして提供されるランタイムライブラリをそのまま使います。

なお、説明の都合上サーバ/クライアントで同じgRPCクライアントを使っていますが、これは異なるものでも構いません。実装言語が異なっていても構いません。

ライブラリ/モジュール選定

gRPCを自身のアプリケーションに適用するにあたり、クライアント/サーバスタブの生成ツールとランタイムライブラリを選定する必要があります。
ただ、それぞれ独立して自由に選べるわけではありません。一般的には、ランタイムライブラリ提供側がそれに対応する生成ツールを提供しています。

以下に今回調査した生成ツール-ランタイムライブラリの組み合わせを列挙します。

No. 生成ツール ランタイムライブラリ 一言コメント
1 grpcio.tools grpcio gRPC公式の組み合わせ
2 grpclib grpclib PurePython実装でよくつかわれる
3 purerpc purerpc trioが管理している
4 betterproto grpclib 生成されるコードが理解しやすい
5 betterproto2 grpclib 生成されるコードが理解しやすい

以降、それぞれの生成ツール/ランタイムについて説明します。

ランタイム

ランタイムの比較表を下記に示します。
GitHub Star/最新リリース日は 2025/12/05 時点のものです。最新リリース日はPyPIでのリリース日です。

No. ランタイム GitHub Star 最新リリース日 実装の特徴
1 grpcio 44050(python以外の言語も含むリポジトリ全体) 2025/10/22 C実装のラッパー
2 grpclib 974 2025/5/5 PurePython実装、非同期対応
3 purerpc 225 2022/4/20 PurePython実装、asyncio以外の非同期ランタイムが使える

それぞれのランタイムについて説明します。

grpcio

gRPC

公式が提供するライブラリで、CベースのコアをPythonから利用する実装。過去には同期APIしか提供していなかったが、現在は非同期(async)APIも提供しています。
リリースは現在も定期的に行われています。

grpclib

grpclib- Pure-Python gRPC implementation for asyncio

Pure Pythonでの実装を特徴とします。非同期APIのサポートはgrpcioよりも早かったようで、インターネット上の古い記事では非同期対応を理由に選択していることがあります。
リリース頻度は低めであり、GitHubのissue上 では積極的な開発はしないと述べられています。

It is used in production but it is not actively developed, it is kinda in maintenance mode unless there will be contributions from the community.

purerpc

purerpc

非同期ランタイムのひとつである trio のチームが開発しています。非同期ランタイムとしてtrio/asyncio/uvloopが使えます。非同期ランタイムを標準のasyncioから変えているアプリケーションでは選択肢に入るかもしれません。

最新のリリースは2022年であり、最終コミットも2024年4月であるため、更新頻度は低め。

性能の比較

grpcioとgrpclibのパフォーマンスについて、llucax/python-grpc-benchmark で実施されています。さらに、このリポジトリのissueで、条件を現実的なものに変えた再測定も行われています(Improper benchmarking of throughput #2)。grpclibはPurePython実装ということでgrpcioより遅い可能性があると考えていましたが、最大でも2倍程度のようです。
詳細はリンク先を確認のうえ、必要に応じて実際のアプリケーションに近い条件でベンチマークを取得してください。

生成ツール

以下に生成ツールの比較表を示します。なお、コードの読みやすさは筆者の主観に基づきます。

No. 生成ツール GitHub Star 最新リリース日 データ型コードの読みやすさ サービスコードの読みやすさ その他の特徴
1 grpcio.tools 44050(grpcioと同じ) 2025/10/22 ×
2 grpclib 974 2025/5/5 ×
3 purerpc 225 2022/4/20 ×
4 betterproto 1746 2020/5/27 サーバ側コードを生成できない
5 betterproto2 113 2025/10/25

生成コードの読みやすさに関する説明をした後、各生成ツールの説明をします。

生成コードの構成と読みやすさ

生成されるコードは2種類のコードを含みます。(ここでの名称は筆者がつけているもので、正式な名称ではない可能性があります。)

  1. データ型コード
    • Protocol Buffersで定義したデータに対応するクラス群
  2. サービスコード
    • Protocol Buffersで定義したサービスに対応するクラス群

grpcio.tools / grpclib / purerpc は 1についてgoogleのProtocol Buffersライブラリによって出力されたものを使います。このライブラリが生成するコードはC実装のラッパーを使うため、理解の難しいコードになっています。pet.protoというproto定義をもとに自動生成した場合のコードを下記に示します。

注意 : 以降の自動生成コードのライセンスは入力の proto ファイルのライセンスに従います

# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: pet/v1/pet.proto
# Protobuf Python Version: 5.27.4
"""Generated protocol buffer code."""
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.PUBLIC,
    5,
    27,
    4,
    '',
    'pet/v1/pet.proto'
)
# @@protoc_insertion_point(imports)

_sym_db = _symbol_database.Default()


from google.type import datetime_pb2 as google_dot_type_dot_datetime__pb2


DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10pet/v1/pet.proto\x12\x06pet.v1\x1a\x1agoogle/type/datetime.proto\"\x92\x01\n\x03Pet\x12*\n\x08pet_type\x18\x01 \x01(\x0e\x32\x0f.pet.v1.PetTypeR\x07petType\x12\x15\n\x06pet_id\x18\x02 \x01(\tR\x05petId\x12\x12\n\x04name\x18\x03 \x01(\tR\x04name\x12\x34\n\ncreated_at\x18\x04 \x01(\x0b\x32\x15.google.type.DateTimeR\tcreatedAt\"&\n\rGetPetRequest\x12\x15\n\x06pet_id\x18\x01 \x01(\tR\x05petId\"/\n\x0eGetPetResponse\x12\x1d\n\x03pet\x18\x01 \x01(\x0b\x32\x0b.pet.v1.PetR\x03pet\"O\n\rPutPetRequest\x12*\n\x08pet_type\x18\x01 \x01(\x0e\x32\x0f.pet.v1.PetTypeR\x07petType\x12\x12\n\x04name\x18\x02 \x01(\tR\x04name\"/\n\x0ePutPetResponse\x12\x1d\n\x03pet\x18\x01 \x01(\x0b\x32\x0b.pet.v1.PetR\x03pet\")\n\x10\x44\x65letePetRequest\x12\x15\n\x06pet_id\x18\x01 \x01(\tR\x05petId\"\x13\n\x11\x44\x65letePetResponse*q\n\x07PetType\x12\x18\n\x14PET_TYPE_UNSPECIFIED\x10\x00\x12\x10\n\x0cPET_TYPE_CAT\x10\x01\x12\x10\n\x0cPET_TYPE_DOG\x10\x02\x12\x12\n\x0ePET_TYPE_SNAKE\x10\x03\x12\x14\n\x10PET_TYPE_HAMSTER\x10\x04\x32\xcb\x01\n\x0fPetStoreService\x12\x39\n\x06GetPet\x12\x15.pet.v1.GetPetRequest\x1a\x16.pet.v1.GetPetResponse\"\x00\x12\x39\n\x06PutPet\x12\x15.pet.v1.PutPetRequest\x1a\x16.pet.v1.PutPetResponse\"\x00\x12\x42\n\tDeletePet\x12\x18.pet.v1.DeletePetRequest\x1a\x19.pet.v1.DeletePetResponse\"\x00\x62\x06proto3')

_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'pet.v1.pet_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
  DESCRIPTOR._loaded_options = None
  _globals['_PETTYPE']._serialized_start=488
  _globals['_PETTYPE']._serialized_end=601
  _globals['_PET']._serialized_start=57
  _globals['_PET']._serialized_end=203
  _globals['_GETPETREQUEST']._serialized_start=205
  _globals['_GETPETREQUEST']._serialized_end=243
  _globals['_GETPETRESPONSE']._serialized_start=245
  _globals['_GETPETRESPONSE']._serialized_end=292
  _globals['_PUTPETREQUEST']._serialized_start=294
  _globals['_PUTPETREQUEST']._serialized_end=373
  _globals['_PUTPETRESPONSE']._serialized_start=375
  _globals['_PUTPETRESPONSE']._serialized_end=422
  _globals['_DELETEPETREQUEST']._serialized_start=424
  _globals['_DELETEPETREQUEST']._serialized_end=465
  _globals['_DELETEPETRESPONSE']._serialized_start=467
  _globals['_DELETEPETRESPONSE']._serialized_end=486
  _globals['_PETSTORESERVICE']._serialized_start=604
  _globals['_PETSTORESERVICE']._serialized_end=807
# @@protoc_insertion_point(module_scope)

一応 pyi ファイルも合わせて生成することで、使い方の推測は可能になります。

from google.type import datetime_pb2 as _datetime_pb2
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, Mapping as _Mapping, Optional as _Optional, Union as _Union

DESCRIPTOR: _descriptor.FileDescriptor

class PetType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
    __slots__ = ()
    PET_TYPE_UNSPECIFIED: _ClassVar[PetType]
    PET_TYPE_CAT: _ClassVar[PetType]
    PET_TYPE_DOG: _ClassVar[PetType]
    PET_TYPE_SNAKE: _ClassVar[PetType]
    PET_TYPE_HAMSTER: _ClassVar[PetType]
PET_TYPE_UNSPECIFIED: PetType
PET_TYPE_CAT: PetType
PET_TYPE_DOG: PetType
PET_TYPE_SNAKE: PetType
PET_TYPE_HAMSTER: PetType

class Pet(_message.Message):
    __slots__ = ("pet_type", "pet_id", "name", "created_at")
    PET_TYPE_FIELD_NUMBER: _ClassVar[int]
    PET_ID_FIELD_NUMBER: _ClassVar[int]
    NAME_FIELD_NUMBER: _ClassVar[int]
    CREATED_AT_FIELD_NUMBER: _ClassVar[int]
    pet_type: PetType
    pet_id: str
    name: str
    created_at: _datetime_pb2.DateTime
    def __init__(self, pet_type: _Optional[_Union[PetType, str]] = ..., pet_id: _Optional[str] = ..., name: _Optional[str] = ..., created_at: _Optional[_Union[_datetime_pb2.DateTime, _Mapping]] = ...) -> None: ...

class GetPetRequest(_message.Message):
    __slots__ = ("pet_id",)
    PET_ID_FIELD_NUMBER: _ClassVar[int]
    pet_id: str
    def __init__(self, pet_id: _Optional[str] = ...) -> None: ...

class GetPetResponse(_message.Message):
    __slots__ = ("pet",)
    PET_FIELD_NUMBER: _ClassVar[int]
    pet: Pet
    def __init__(self, pet: _Optional[_Union[Pet, _Mapping]] = ...) -> None: ...

class PutPetRequest(_message.Message):
    __slots__ = ("pet_type", "name")
    PET_TYPE_FIELD_NUMBER: _ClassVar[int]
    NAME_FIELD_NUMBER: _ClassVar[int]
    pet_type: PetType
    name: str
    def __init__(self, pet_type: _Optional[_Union[PetType, str]] = ..., name: _Optional[str] = ...) -> None: ...

class PutPetResponse(_message.Message):
    __slots__ = ("pet",)
    PET_FIELD_NUMBER: _ClassVar[int]
    pet: Pet
    def __init__(self, pet: _Optional[_Union[Pet, _Mapping]] = ...) -> None: ...

class DeletePetRequest(_message.Message):
    __slots__ = ("pet_id",)
    PET_ID_FIELD_NUMBER: _ClassVar[int]
    pet_id: str
    def __init__(self, pet_id: _Optional[str] = ...) -> None: ...

class DeletePetResponse(_message.Message):
    __slots__ = ()
    def __init__(self) -> None: ...

betterproto / betterproto2 は1, 2ともに独自に出力します。dataclassなどのPythonの機能を使っており、Pythonを使っているソフトウェアエンジニアであれば読みやすく感じると思います。以下にbetterprotoによる自動生成結果の抜粋を示します。

# Generated by the protocol buffer compiler.  DO NOT EDIT!
# sources: pet/v1/pet.proto
# plugin: python-betterproto
from dataclasses import dataclass

import betterproto
import grpclib


class PetType(betterproto.Enum):
    """PetType represents the different types of pets in the pet store."""

    PET_TYPE_UNSPECIFIED = 0
    PET_TYPE_CAT = 1
    PET_TYPE_DOG = 2
    PET_TYPE_SNAKE = 3
    PET_TYPE_HAMSTER = 4


@dataclass
class Pet(betterproto.Message):
    """Pet represents a pet in the pet store."""

    pet_type: "PetType" = betterproto.enum_field(1)
    pet_id: str = betterproto.string_field(2)
    name: str = betterproto.string_field(3)
    created_at: type.DateTime = betterproto.message_field(4)


@dataclass
class GetPetRequest(betterproto.Message):
    pet_id: str = betterproto.string_field(1)


@dataclass
class GetPetResponse(betterproto.Message):
    pet: "Pet" = betterproto.message_field(1)


@dataclass
class PutPetRequest(betterproto.Message):
    pet_type: "PetType" = betterproto.enum_field(1)
    name: str = betterproto.string_field(2)


@dataclass
class PutPetResponse(betterproto.Message):
    pet: "Pet" = betterproto.message_field(1)


@dataclass
class DeletePetRequest(betterproto.Message):
    pet_id: str = betterproto.string_field(1)


@dataclass
class DeletePetResponse(betterproto.Message):
    pass

サービス側はどのツールも読みやすさについて大差はありませんでした。

なお、読みやすいコードはコード生成部のバグに人間が気付きやすい、利用方法を推測しやすいというメリットがある一方で、自動生成されるコードに利用者が手を入れることは少ないので、そもそも読みやすさが必要なのかは議論の余地があると思います。

grpcio.tools / grpclib / purerpc

それぞれランタイムと合わせて提供されています。
生成ツールとしては大きな特徴はありません。

betterproto

danielgtaylor/python-betterproto

grpclibをバックエンドに、Pythonのモダンな機能(dataclass/typing)を使ってきれいなコードを生成することを目的としたツール。
読みやすいコードが出力されるものの、難点としてサーバ側コードが自動生成できない、後継のbetterproto2の開発に移行している点があります。

betterproto2

betterproto/python-betterproto2

betterprotoの作者によって開発されているbetterprotoの後継。サーバ側コードの生成をサポートしており、さらにPydantic モデルの生成をします。現在は開発中の状況で、破壊的な変更が起こりうると述べられています。

状況ごとの選択案

状況に応じて複数パターン考えられます。

  • 長期にわたってメンテナンスが行われることを重視する
    • grpcio が適している。公式がサポートを続けると考えられる
  • 生成されるコードの読みやすさを重視する
    • betterproto/betterproto2 + grpclib が適している。メンテナンスがされていないものの十分な実績のあるbetterprotoか、現在開発が進んでいるbetterproto2かは好みで選択する
  • asyncio以外の非同期ランタイムを使う
    • purerpc が適している。

アプリケーション/プロジェクトの性質に応じて、適切なライブラリを選ぶ必要があります。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?