#0162（2025/06/03）DESCRIPTORとは

# DESCRIPTORとは何か

Protocol Buffers（以下、protobuf）で自動生成されたPythonクラスには、
`DESCRIPTOR`というクラス属性が付随します。これは、.protoファイルに定義されたメッセージ構造をメタデータとして持つオブジェクトであり、以下のような情報を提供します。

* フィールド名やフィールド番号、フィールド型
* リピート／オプショナルのラベル情報
* ネストされたメッセージやEnumの構造

本稿では、エンジニア向けに「基本的な使い方」から「応用的なリフレクション操作」までを、
サンプルコードを交えながら解説します。

---

## 1. 基本的なイメージと構造

### 1.1 DESCRIPTORの位置付け

* `.proto` で定義したメッセージ型は、`protoc`によってPythonクラスに変換される。
* 変換後のクラスには、ステートレスなメタ情報を保持する `DESCRIPTOR` 属性がある。
* `DESCRIPTOR` は `google.protobuf.descriptor.Descriptor` クラスのインスタンスで、次のような情報をもつ。

  * `.fields` : メッセージ内のすべてのフィールドを表す `FieldDescriptor` のリスト
  * `.fields_by_name` : キーがフィールド名、値が `FieldDescriptor` の辞書
  * `.nested_types` : ネストされたメッセージ定義を `Descriptor` として持つリスト
  * `.enum_types` : メッセージ内のEnum型定義を表すリスト

```python
# sample_pb2.py で定義された Message クラス例
def print_descriptor_info():
    from sample_pb2 import MyMessage
    desc = MyMessage.DESCRIPTOR
    print(type(desc))  # <class 'google.protobuf.descriptor.Descriptor'>
    print([f.name for f in desc.fields])
    # 出力例: ['id', 'name', 'tags']
```

### 1.2 FieldDescriptor の中身

* メタ情報を個別に参照したいときは、各 `FieldDescriptor` を見る。
* 主なプロパティ：

  * `name`（フィールド名）
  * `number`（proto上のタグ番号）
  * `type`（型番号。例えば 9=string, 5=int32, 11=message など）
  * `label`（optional/repeated の情報）
  * `message_type`（型が message のときは、その `Descriptor` オブジェクト）

```python
from sample_pb2 import MyMessage
fd = MyMessage.DESCRIPTOR.fields_by_name['id']
print(fd.name, fd.number, fd.type, fd.label)
# id 1 5 1  （型番号5=int32、label=1 は optional）
```

---

## 2. 基本的な使い方サンプル

ここでは、典型的なユースケースをいくつか紹介します。すべての例では、`sample.proto` に以下のような定義があるとします。

```proto
message Address {
  string city = 1;
  int32 zip = 2;
}

message Person {
  int32 id = 1;
  string name = 2;
  repeated string tags = 3;
  Address address = 4;
}
```

### 2.1 フィールド一覧を取得する

```python
from sample_pb2 import Person
# フィールド名リストだけを得る
field_names = [f.name for f in Person.DESCRIPTOR.fields]
print(field_names)  # ['id', 'name', 'tags', 'address']
```

### 2.2 フィールド型やラベルを調べる

```python
from sample_pb2 import Person
for f in Person.DESCRIPTOR.fields:
    print(f"{f.name}: type={f.type}, label={f.label}")
# id: type=5, label=1
# name: type=9, label=1
# tags: type=9, label=3  (label 3=repeated)
# address: type=11, label=1 (type 11 → message)
```

### 2.3 ネストされたメッセージ情報を参照する

```python
from sample_pb2 import Person
addr_fd = Person.DESCRIPTOR.fields_by_name['address']
# address フィールドは message 型なので、message_type に子 Descriptor が格納される
nested_desc = addr_fd.message_type
print([f.name for f in nested_desc.fields])
# ['city', 'zip']
```

以上で「フィールドを列挙して、型やネスト構造を把握する」という基本操作は完了です。

---

## 3. 応用的リフレクション操作

### 3.1 動的メッセージ生成と値設定

`DESCRIPTOR` を使えば、メッセージクラスを動的に扱うことができます。以下は、文字列で型名を指定し、メッセージを生成して値を埋める例です。

```python
from google.protobuf import symbol_database
# symbol_database.Default() は、登録済みのすべてのメッセージクラスを扱えるレジストリ
sym_db = symbol_database.Default()

# 文字列でクラスを取得
msg_cls = sym_db.GetSymbol('sample_pb2.Person')
# インスタンス生成
person = msg_cls()
# DESCRIPTOR を通じてフィールドにアクセス
for f in person.DESCRIPTOR.fields:
    # 履歴や外部設定に基づいて動的に値設定可能
    if f.type == 5:  # int32
        setattr(person, f.name, 42)
    elif f.type == 9:  # string
        if f.label == 3:  # repeated
            getattr(person, f.name).extend(['tag1', 'tag2'])
        else:
            setattr(person, f.name, 'Alice')
    elif f.type == 11:  # message
        sub_msg = getattr(person, f.name)
        # サブメッセージにも同じロジックで値を付与
        setattr(sub_msg, 'city', 'Tokyo')
        setattr(sub_msg, 'zip', 100-0001)

print(person)
# id: 42
# name: "Alice"
# tags: "tag1", "tag2"
# address {
#   city: "Tokyo"
#   zip: 1000001
# }
```

### 3.2 動的インスペクションとバリデーション

フィールドに付加されたオプション情報や `HasField`, `ListFields` を活用し、
JSON スキーマ生成やバリデーションを行う例です。

```python
from sample_pb2 import Person
import json

def to_json(pb_obj):
    # フィールド名＋値を dict に集める
    data = {}
    for f in pb_obj.DESCRIPTOR.fields:
        if f.label == 3:  # repeated
            data[f.name] = list(getattr(pb_obj, f.name))
        elif f.type == 11:  # nested message
            if pb_obj.HasField(f.name):
                data[f.name] = to_json(getattr(pb_obj, f.name))
        else:
            if pb_obj.HasField(f.name):
                data[f.name] = getattr(pb_obj, f.name)
    return data

p = Person(id=1, name='Bob', tags=['x', 'y'])
json_str = json.dumps(to_json(p), ensure_ascii=False)
print(json_str)
# {"id": 1, "name": "Bob", "tags": ["x", "y"]}
```

### 3.3 ネスト構造の再帰的トラバース

`DESCRIPTOR.nested_types` を使えば、メッセージ内の更に深い階層を横断できます。
以下は、任意のメッセージ型に対し、すべてのネストされた型のフィールド情報を再帰的に列挙する例です。

```python
from sample_pb2 import Person

def print_all_descriptors(descriptor, indent=0):
    prefix = ' ' * indent
    print(f"{prefix}Message: {descriptor.full_name}")
    for f in descriptor.fields:
        print(f"{prefix}  {f.name} (type={f.type}, label={f.label})")
        # ネストされたメッセージ型かチェック
        if f.type == 11:
            print_all_descriptors(f.message_type, indent + 4)

print_all_descriptors(Person.DESCRIPTOR)
# Message: sample_pb2.Person
#   id (type=5, label=1)
#   name (type=9, label=1)
#   tags (type=9, label=3)
#   address (type=11, label=1)
#     Message: sample_pb2.Address
#       city (type=9, label=1)
#       zip (type=5, label=1)
```

---

## 4. 高度な活用例

### 4.1 ランタイムでスキーマ変更を扱うケース

サービス間でProtobufのスキーマが頻繁に変わる場合、クライアント側で動的にフィールドを扱いたいことがあります。
以下は、メッセージを受信し、想定外のフィールドが存在したら警告ログを出す例です。

```python
from sample_pb2 import Person
import logging

# 受信バイト列をデコード
p = Person()
p.ParseFromString(received_bytes)

# 受信したすべてのフィールドと型番号を取得
for field, value in p.ListFields():
    # ListFields は (FieldDescriptor, 値) のタプルを返す
    if field.name not in [f.name for f in Person.DESCRIPTOR.fields]:
        logging.warning(f"未知のフィールドを受信: {field.name}")
    else:
        # 型に応じた通常処理を行う
        process(field.name, value)
```

### 4.2 JSON と Protobuf を動的に相互変換

フロントエンドとやり取りするときに、Protobuf メッセージをJSONに変換したり、その逆を動的に行うケースでは、`DESCRIPTOR` を使ったジェネリック処理が便利です。

```python
from google.protobuf import json_format
from sample_pb2 import Person

# 例: dict⇒Protobuf
data = {'id': 2, 'name': 'Carol', 'tags': ['a']}
p = Person()
for k, v in data.items():
    # フィールド存在チェック
    if k in Person.DESCRIPTOR.fields_by_name:
        fdesc = Person.DESCRIPTOR.fields_by_name[k]
        if fdesc.type == 11:
            # ネストメッセージは再帰的に変換
            sub_msg = getattr(p, k)
            sub_dict_to_pb(v, sub_msg)
        else:
            setattr(p, k, v)
    else:
        # 未知のフィールドは無視またはロギング
        pass

# 例: Protobuf⇒JSON文字列
json_str = json_format.MessageToJson(p, including_default_value_fields=False, preserving_proto_field_name=True)
print(json_str)
```

### 4.3 gRPCサーバでスキーマ情報を動的に参照

gRPCサーバをPythonで実装している場合、受信メッセージの型を直接書かずに、`DESCRIPTOR` 情報を使って、
エラーコードやメトリクスに動的タグを付与する例。

```python
import grpc
from concurrent import futures
from sample_pb2 import Person
from sample_pb2_grpc import PersonServiceServicer, add_PersonServiceServicer_to_server

class DynamicService(PersonServiceServicer):
    def GetPerson(self, request, context):
        # リクエストの DESCRIPTOR から型情報を取得
        msg_name = request.DESCRIPTOR.full_name
        field_names = [f.name for f in request.DESCRIPTOR.fields]
        # メトリクスにタグとして追加
        metrics_counter.labels(message=msg_name, fields=','.join(field_names)).inc()
        # 処理ロジックを呼び出し
        return generate_person_response(request)

server = grpc.server(futures.ThreadPoolExecutor(max_workers=4))
add_PersonServiceServicer_to_server(DynamicService(), server)
server.add_insecure_port('[::]:50051')
server.start()
```

---

## 5. 注意点とベストプラクティス

1. **スキーマの互換性を維持する**

   * `DESCRIPTOR` を使って動的に値を設定・取得する場合、バージョンアップ時にフィールド名や型が変わると例外が発生しやすい。
   * `.proto` の互換ルール（後方互換／前方互換）を遵守する。

2. **リフレクションはデバッグやツール向けに限定する**

   * ランタイムで `DESCRIPTOR` を毎回走査するコードは、パフォーマンスに影響しやすい。
   * 本番処理では可能な限り静的にフィールドアクセスする方が早い。

3. **フィールドタイプとラベルの意味を理解する**

   * `label == 3` は `repeated`、`label == 1` は `optional` 相当。
   * `type == 11` はネストしたメッセージ型。文字列や数値と扱いが異なるため注意。

4. **ネストが深くなる設計は注意**

   * 複数階層のネストメッセージを頻繁にリフレクションすると、コードが複雑化しデバッグが難しくなる。

---

## 6. まとめ

* `DESCRIPTOR` は、**プロトコルバッファのメッセージ定義を動的に扱うための入り口** であり、

  * フィールド一覧やネスト構造をプログラムから取得できる
  * 型情報を使った動的処理やバリデーション、JSON相互変換に役立つ
  * gRPC や汎用ツール実装において強力な武器となる

* 一方、**反射を多用した実装はパフォーマンスや可読性を低下させるリスク**があるため、

  * デバッグ／ツール開発向けに限定するか
  * キャッシュを活用してリフレクション結果を使い回すなど工夫が必要

以上が、エンジニア向けの `DESCRIPTOR` に関する総合的な解説です。**基本から応用まで網羅**しているので、プロジェクト内で Protobuf を動的に活用したい場合や、JSON⇔Protobuf の変換ツールを作る際などに参照してください。

---

> 参考用語解説：
>
> * **Reflection**: プログラム実行時に型やフィールド情報を取得し、動的にオブジェクトを操作する仕組み。
> * **gRPC**: Google が開発した RPC フレームワーク。Protocol Buffers を IDL として採用し、高速な通信と型安全性を実現する。
> * **JSON⇔Protobuf**: Protobuf のバイナリ形式を JSON に変換することで、人間が読みやすい形式にしつつ、型情報を保持できる。