Python 程式碼產生指南

精確描述了協定緩衝區編譯器針對任何給定的協定定義所產生的 Python 定義。

proto2 和 proto3 產生程式碼之間的任何差異都會被突顯出來 - 請注意,這些差異在於本文檔中描述的產生程式碼,而不是基本訊息類別/介面,它們在這兩個版本中是相同的。在閱讀本文檔之前,您應該先閱讀 proto2 語言指南 和/或 proto3 語言指南

Python Protocol Buffers 實作與 C++ 和 Java 有些不同。在 Python 中,編譯器僅輸出程式碼以建構產生類別的描述符,而 Python metaclass 執行實際工作。本文檔描述了在套用 metaclass *之後* 您獲得的內容。

編譯器調用

當使用 --python_out= 命令列旗標調用時,協定緩衝區編譯器會產生 Python 輸出。--python_out= 選項的參數是您希望編譯器寫入 Python 輸出的目錄。編譯器會為每個 .proto 檔案輸入建立一個 .py 檔案。輸出檔案的名稱是透過取得 .proto 檔案的名稱並進行兩項變更來計算的

  • 副檔名 (.proto) 會被取代為 _pb2.py
  • proto 路徑 (使用 --proto_path=-I 命令列旗標指定) 會被取代為輸出路徑 (使用 --python_out= 旗標指定)。

因此,例如,假設您如下調用編譯器

protoc --proto_path=src --python_out=build/gen src/foo.proto src/bar/baz.proto

編譯器將讀取檔案 src/foo.protosrc/bar/baz.proto 並產生兩個輸出檔案:build/gen/foo_pb2.pybuild/gen/bar/baz_pb2.py。如果需要,編譯器將自動建立目錄 build/gen/bar,但它*不會*建立 buildbuild/gen;它們必須已經存在。

Protoc 可以使用 --pyi_out 參數產生 Python 存根 (.pyi)。

請注意,如果 .proto 檔案或其路徑包含任何無法在 Python 模組名稱中使用的字元 (例如,連字號),它們將被取代為底線。因此,檔案 foo-bar.proto 會變成 Python 檔案 foo_bar_pb2.py

套件

協定緩衝區編譯器產生的 Python 程式碼完全不受 .proto 檔案中定義的套件名稱影響。相反地,Python 套件是由目錄結構識別的。

訊息

給定一個簡單的訊息宣告

message Foo {}

協定緩衝區編譯器會產生一個名為 Foo 的類別,它繼承自 google.protobuf.Message。該類別是一個具體類別;沒有抽象方法未實作。與 C++ 和 Java 不同,Python 產生程式碼不受 .proto 檔案中 optimize_for 選項的影響;實際上,所有 Python 程式碼都針對程式碼大小進行了最佳化。

如果訊息的名稱是 Python 關鍵字,則其類別只能透過 getattr() 存取,如 與 Python 關鍵字衝突的名稱 章節中所述。

您*不應該*建立自己的 Foo 子類別。產生的類別並非設計用於子類別化,並且可能會導致「脆弱的基底類別」問題。此外,實作繼承是不良設計。

Python 訊息類別除了 Message 介面定義的成員以及為巢狀欄位、訊息和列舉類型產生的成員 (如下所述) 之外,沒有其他特定的公用成員。Message 提供了可用於檢查、操作、讀取或寫入整個訊息的方法,包括從二進位字串剖析和序列化到二進位字串。除了這些方法之外,Foo 類別還定義了以下靜態方法

  • FromString(s):傳回從給定字串還原序列化的新訊息實例。

請注意,您也可以使用 text_format 模組來處理文字格式的協定訊息:例如,Merge() 方法可讓您將訊息的 ASCII 表示法合併到現有訊息中。

巢狀類型

訊息可以在另一個訊息內部宣告。例如

message Foo {
  message Bar {}
}

在這種情況下,Bar 類別被宣告為 Foo 的靜態成員,因此您可以將其稱為 Foo.Bar

廣為人知的類型

Protocol buffers 提供了許多 廣為人知的類型,您可以在 .proto 檔案中與您自己的訊息類型一起使用。某些 WKT 訊息除了通常的協定緩衝區訊息方法之外,還具有特殊方法,因為它們同時繼承自 google.protobuf.Message 和 WKT 類別。

Any

對於 Any 訊息,您可以呼叫 Pack() 將指定的訊息封裝到目前的 Any 訊息中,或呼叫 Unpack() 將目前的 Any 訊息解封裝到指定的訊息中。例如

any_message.Pack(message)
any_message.Unpack(message)

Unpack() 還會檢查傳入訊息物件的描述符是否與儲存的描述符相符,如果不相符則傳回 False,並且不會嘗試任何解封裝;否則傳回 True

您也可以呼叫 Is() 方法來檢查 Any 訊息是否代表給定的協定緩衝區類型。例如

assert any_message.Is(message.DESCRIPTOR)

使用 TypeName() 方法來檢索內部訊息的 protobuf 類型名稱。

Timestamp

Timestamp 訊息可以使用 ToJsonString()/FromJsonString() 方法轉換為/自 RFC 3339 日期字串格式 (JSON 字串)。例如

timestamp_message.FromJsonString("1970-01-01T00:00:00Z")
assert timestamp_message.ToJsonString() == "1970-01-01T00:00:00Z"

您也可以呼叫 GetCurrentTime() 以目前時間填寫 Timestamp 訊息

timestamp_message.GetCurrentTime()

若要在自 epoch 以來的其他時間單位之間轉換,您可以呼叫 ToNanoseconds(), FromNanoseconds(), ToMicroseconds(), FromMicroseconds(), ToMilliseconds(), FromMilliseconds(), ToSeconds()FromSeconds()。產生的程式碼也具有 ToDatetime()FromDatetime() 方法,可在 Python datetime 物件和 Timestamp 之間轉換。例如

timestamp_message.FromMicroseconds(-1)
assert timestamp_message.ToMicroseconds() == -1
dt = datetime(2016, 1, 1)
timestamp_message.FromDatetime(dt)
self.assertEqual(dt, timestamp_message.ToDatetime())

Duration

Duration 訊息具有與 Timestamp 相同的方法,可在 JSON 字串和其他時間單位之間轉換。若要在 timedelta 和 Duration 之間轉換,您可以呼叫 ToTimedelta()FromTimedelta。例如

duration_message.FromNanoseconds(1999999999)
td = duration_message.ToTimedelta()
assert td.seconds == 1
assert td.microseconds == 999999

FieldMask

FieldMask 訊息可以使用 ToJsonString()/FromJsonString() 方法轉換為/自 JSON 字串。此外,FieldMask 訊息還具有以下方法

  • IsValidForDescriptor(message_descriptor):檢查 FieldMask 對於訊息描述符是否有效。
  • AllFieldsFromDescriptor(message_descriptor):將訊息描述符的所有直接欄位取得至 FieldMask。
  • CanonicalFormFromMask(mask):將 FieldMask 轉換為標準形式。
  • Union(mask1, mask2):將兩個 FieldMask 合併到此 FieldMask 中。
  • Intersect(mask1, mask2):將兩個 FieldMask 交集到此 FieldMask 中。
  • MergeMessage(source, destination, replace_message_field=False, replace_repeated_field=False):將 FieldMask 中指定的欄位從來源合併到目的地。

Struct

Struct 訊息可讓您直接取得和設定項目。例如

struct_message["key1"] = 5
struct_message["key2"] = "abc"
struct_message["key3"] = True

若要取得或建立清單/struct,您可以呼叫 get_or_create_list()/get_or_create_struct()。例如

struct.get_or_create_struct("key4")["subkey"] = 11.0
struct.get_or_create_list("key5")

ListValue

ListValue 訊息的作用類似於 Python 序列,可讓您執行以下操作

list_value = struct_message.get_or_create_list("key")
list_value.extend([6, "seven", True, None])
list_value.append(False)
assert len(list_value) == 5
assert list_value[0] == 6
assert list_value[1] == "seven"
assert list_value[2] == True
assert list_value[3] == None
assert list_Value[4] == False

若要新增 ListValue/Struct,請呼叫 add_list()/add_struct()。例如

list_value.add_struct()["key"] = 1
list_value.add_list().extend([1, "two", True])

欄位

對於訊息類型中的每個欄位,對應的類別都有一個與欄位名稱相同的屬性。您如何操作屬性取決於其類型。

除了屬性之外,編譯器還會為每個欄位產生一個整數常數,其中包含其欄位編號。常數名稱是轉換為大寫的欄位名稱,後跟 _FIELD_NUMBER。例如,給定欄位 optional int32 foo_bar = 5;,編譯器將產生常數 FOO_BAR_FIELD_NUMBER = 5

如果欄位的名稱是 Python 關鍵字,則其屬性只能透過 getattr()setattr() 存取,如 與 Python 關鍵字衝突的名稱 章節中所述。

單數欄位 (proto2)

如果您有一個單數 (optional 或 required) 欄位 foo,其類型為任何非訊息類型,您可以像操作一般欄位一樣操作欄位 foo。例如,如果 foo 的類型為 int32,您可以說

message.foo = 123
print(message.foo)

請注意,將 foo 設定為錯誤類型的值會引發 TypeError

如果讀取未設定的 foo,則其值是該欄位的預設值。若要檢查是否已設定 foo,或清除 foo 的值,您必須呼叫 Message 介面的 HasField()ClearField() 方法。例如

assert not message.HasField("foo")
message.foo = 123
assert message.HasField("foo")
message.ClearField("foo")
assert not message.HasField("foo")

單數欄位 (proto3)

如果您有一個單數欄位 foo,其類型為任何非訊息類型,您可以像操作一般欄位一樣操作欄位 foo。例如,如果 foo 的類型為 int32,您可以說

message.foo = 123
print(message.foo)

請注意,將 foo 設定為錯誤類型的值會引發 TypeError

如果讀取未設定的 foo,則其值是該欄位的預設值。若要清除 foo 的值並將其重設為其類型的預設值,您可以呼叫 Message 介面的 ClearField() 方法。例如

message.foo = 123
message.ClearField("foo")

單數訊息欄位

訊息類型的工作方式略有不同。您無法將值指派給嵌入式訊息欄位。相反地,將值指派給子訊息中的任何欄位都表示在父訊息中設定訊息欄位。您也可以使用父訊息的 HasField() 方法來檢查是否已設定訊息類型欄位值。

因此,例如,假設您有以下 .proto 定義

message Foo {
  optional Bar bar = 1;
}
message Bar {
  optional int32 i = 1;
}

您*無法*執行以下操作

foo = Foo()
foo.bar = Bar()  # WRONG!

相反地,若要設定 bar,您只需直接將值指派給 bar 內的欄位,然後 - 瞧!- foo 就有一個 bar 欄位

foo = Foo()
assert not foo.HasField("bar")
foo.bar.i = 1
assert foo.HasField("bar")
assert foo.bar.i == 1
foo.ClearField("bar")
assert not foo.HasField("bar")
assert foo.bar.i == 0  # Default value

同樣地,您可以使用 Message 介面的 CopyFrom() 方法來設定 bar。這會從與 bar 相同類型的另一個訊息複製所有值。

foo.bar.CopyFrom(baz)

請注意,僅僅讀取 bar 內的欄位*不會*設定該欄位

foo = Foo()
assert not foo.HasField("bar")
print(foo.bar.i)  # Print i's default value
assert not foo.HasField("bar")

如果您需要訊息上沒有您可以或想要設定的任何欄位的「has」位元,您可以使用 SetInParent() 方法。

foo = Foo()
assert not foo.HasField("bar")
foo.bar.SetInParent()  # Set Foo.bar to a default Bar message
assert foo.HasField("bar")

重複欄位

重複欄位有三種類型:純量、列舉和訊息。Map 欄位和 oneof 欄位不能重複。

重複純量和列舉欄位

重複欄位表示為一個物件,其作用類似於 Python 序列。與嵌入式訊息一樣,您無法直接指派欄位,但您可以操作它。例如,給定此訊息定義

message Foo {
  repeated int32 nums = 1;
}

您可以執行以下操作

foo = Foo()
foo.nums.append(15)        # Appends one value
foo.nums.extend([32, 47])  # Appends an entire list

assert len(foo.nums) == 3
assert foo.nums[0] == 15
assert foo.nums[1] == 32
assert foo.nums == [15, 32, 47]

foo.nums[:] = [33, 48]     # Assigns an entire list
assert foo.nums == [33, 48]

foo.nums[1] = 56    # Reassigns a value
assert foo.nums[1] == 56
for i in foo.nums:  # Loops and print
  print(i)
del foo.nums[:]     # Clears list (works just like in a Python list)

Message 介面的 ClearField() 方法除了使用 Python del 之外也有效。

當使用索引來檢索值時,您可以使用負數,例如使用 -1 來檢索清單中的最後一個元素。如果您的索引超出範圍,您將會收到 IndexError: list index out of range

重複訊息欄位

重複訊息欄位的工作方式與重複純量欄位類似。但是,對應的 Python 物件也有一個 add() 方法,可建立新的訊息物件、將其附加到清單中,並傳回它以供呼叫者填寫。此外,物件的 append() 方法會建立給定訊息的副本,並將該副本附加到清單中。這樣做是為了讓訊息始終由父訊息擁有,以避免循環參考以及當可變資料結構有多個擁有者時可能發生的其他混淆。同樣地,物件的 extend() 方法會附加整個訊息清單,但會建立清單中每個訊息的副本

例如,給定此訊息定義

message Foo {
  repeated Bar bars = 1;
}
message Bar {
  optional int32 i = 1;
  optional int32 j = 2;
}

您可以執行以下操作

foo = Foo()
bar = foo.bars.add()        # Adds a Bar then modify
bar.i = 15
foo.bars.add().i = 32       # Adds and modify at the same time
new_bar = Bar()
new_bar.i = 40
another_bar = Bar()
another_bar.i = 57
foo.bars.append(new_bar)        # Uses append() to copy
foo.bars.extend([another_bar])  # Uses extend() to copy

assert len(foo.bars) == 4
assert foo.bars[0].i == 15
assert foo.bars[1].i == 32
assert foo.bars[2].i == 40
assert foo.bars[2] == new_bar      # The appended message is equal,
assert foo.bars[2] is not new_bar  # but it is a copy!
assert foo.bars[3].i == 57
assert foo.bars[3] == another_bar      # The extended message is equal,
assert foo.bars[3] is not another_bar  # but it is a copy!

foo.bars[1].i = 56    # Modifies a single element
assert foo.bars[1].i == 56
for bar in foo.bars:  # Loops and print
  print(bar.i)
del foo.bars[:]       # Clears list

# add() also forwards keyword arguments to the concrete class.
# For example, you can do:

foo.bars.add(i=12, j=13)

# Initializers forward keyword arguments to a concrete class too.
# For example:

foo = Foo(             # Creates Foo
  bars=[               # with its field bars set to a list
    Bar(i=15, j=17),   # where each list member is also initialized during creation.
    Bar(i=32),
    Bar(i=47, j=77),
  ]
)

assert len(foo.bars) == 3
assert foo.bars[0].i == 15
assert foo.bars[0].j == 17
assert foo.bars[1].i == 32
assert foo.bars[2].i == 47
assert foo.bars[2].j == 77

與重複純量欄位不同,重複訊息欄位支援項目指派 (即 __setitem__)。例如

foo = Foo()
foo.bars.add(i=3)
# WRONG!
foo.bars[0] = Bar(i=15)  # Raises an exception
# WRONG!
foo.bars[:] = [Bar(i=15), Bar(i=17)]  # Also raises an exception
# WRONG!
# AttributeError: Cannot delete field attribute
del foo.bars
# RIGHT
del foo.bars[:]
foo.bars.extend([Bar(i=15), Bar(i=17)])

群組 (proto2)

請注意,群組已棄用,建立新的訊息類型時不應使用 - 請改用巢狀訊息類型。

群組將巢狀訊息類型和欄位組合到單個宣告中,並為訊息使用不同的 線路格式。產生的訊息與群組同名。產生的欄位名稱是群組名稱的小寫名稱。

例如,除了線路格式之外,以下兩個訊息定義是等效的

// Version 1: Using groups
message SearchResponse {
  repeated group SearchResult = 1 {
    optional string url = 1;
  }
}
// Version 2: Not using groups
message SearchResponse {
  message SearchResult {
    optional string url = 1;
  }
  repeated SearchResult searchresult = 1;
}

群組可以是 requiredoptionalrepeated。Required 或 optional 群組使用與一般單數訊息欄位相同的 API 進行操作。Repeated 群組使用與一般重複訊息欄位相同的 API 進行操作。

例如,給定上述 SearchResponse 定義,您可以執行以下操作

resp = SearchResponse()
resp.searchresult.add(url="https://blog.google")
assert resp.searchresult[0].url == "https://blog.google"
assert resp.searchresult[0] == SearchResponse.SearchResult(url="https://blog.google")

Map 欄位

給定此訊息定義

message MyMessage {
  map<int32, int32> mapfield = 1;
}

map 欄位的產生 Python API 就像 Python dict 一樣

# Assign value to map
m.mapfield[5] = 10

# Read value from map
m.mapfield[5]

# Iterate over map keys
for key in m.mapfield:
  print(key)
  print(m.mapfield[key])

# Test whether key is in map:
if 5 in m.mapfield:
  print(Found!”)

# Delete key from map.
del m.mapfield[key]

嵌入式訊息欄位 一樣,訊息不能直接指派到 map 值中。相反地,若要將訊息新增為 map 值,您可以參考一個 未定義的鍵,它會建構並傳回新的子訊息

m.message_map[key].submessage_field = 10

您可以在下一節中找到有關未定義鍵的更多資訊。

參考未定義的鍵

當涉及到未定義的鍵時,Protocol Buffer map 的語意與 Python dict 的行為略有不同。在一般 Python dict 中,參考未定義的鍵會引發 KeyError 例外

>>> x = {}
>>> x[5]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 5

但是,在 Protocol Buffers map 中,參考未定義的鍵會在 map 中建立鍵,其值為零/false/空值。此行為更像 Python 標準程式庫 defaultdict

>>> dict(m.mapfield)
{}
>>> m.mapfield[5]
0
>>> dict(m.mapfield)
{5: 0}

對於具有訊息類型值的 map 來說,此行為尤其方便,因為您可以直接更新傳回訊息的欄位。

>>> m.message_map[5].foo = 3

請注意,即使您沒有將任何值指派給訊息欄位,子訊息仍然會在 map 中建立

>>> m.message_map[10]
<test_pb2.M2 object at 0x7fb022af28c0>
>>> dict(m.message_map)
{10: <test_pb2.M2 object at 0x7fb022af28c0>}

這與一般的 嵌入式訊息欄位不同,在嵌入式訊息欄位中,只有在您將值指派給其欄位之一後才會建立訊息本身。

由於對於任何閱讀您的程式碼的人來說,m.message_map[10] 單獨存在,例如,可能會建立子訊息,因此可能不明顯,我們也提供了一個 get_or_create() 方法,它執行相同的操作,但其名稱使可能的訊息建立更加明確

# Equivalent to:
#   m.message_map[10]
# but more explicit that the statement might be creating a new
# empty message in the map.
m.message_map.get_or_create(10)

列舉

在 Python 中,列舉只是整數。定義了一組與列舉的定義值相對應的整數常數。例如,給定

message Foo {
  enum SomeEnum {
    VALUE_A = 0;
    VALUE_B = 5;
    VALUE_C = 1234;
  }
  optional SomeEnum bar = 1;
}

常數 VALUE_AVALUE_BVALUE_C 分別定義為值 0、5 和 1234。如果需要,您可以存取 SomeEnum。如果列舉是在外部範圍中定義的,則這些值是模組常數;如果它是在訊息中定義的 (如上所示),則它們會成為該訊息類別的靜態成員。

例如,您可以透過以下三種方式存取 proto 中以下列舉的值

enum SomeEnum {
    VALUE_A = 0;
    VALUE_B = 5;
    VALUE_C = 1234;
}
value_a = myproto_pb2.SomeEnum.VALUE_A
# or
myproto_pb2.VALUE_A
# or
myproto_pb2.SomeEnum.Value('VALUE_A')

列舉欄位的工作方式與純量欄位完全相同。

foo = Foo()
foo.bar = Foo.VALUE_A
assert foo.bar == 0
assert foo.bar == Foo.VALUE_A

如果列舉的名稱 (或列舉值) 是 Python 關鍵字,則其物件 (或列舉值的屬性) 只能透過 getattr() 存取,如 與 Python 關鍵字衝突的名稱 章節中所述。

您可以在列舉中設定的值取決於您的 protocol buffers 版本

  • proto2 中,列舉不能包含除為列舉類型定義的值以外的數值。如果您指派的值不在列舉中,則產生的程式碼將會擲回例外。
  • proto3 使用開放列舉語意:列舉欄位可以包含任何 int32 值。

列舉有許多實用方法,可用於從值取得欄位名稱,反之亦然,欄位清單等等 - 這些方法在 enum_type_wrapper.EnumTypeWrapper (產生列舉類別的基底類別) 中定義。因此,例如,如果您在 myproto.proto 中有以下獨立列舉

enum SomeEnum {
    VALUE_A = 0;
    VALUE_B = 5;
    VALUE_C = 1234;
}

…您可以執行此操作

self.assertEqual('VALUE_A', myproto_pb2.SomeEnum.Name(myproto_pb2.VALUE_A))
self.assertEqual(5, myproto_pb2.SomeEnum.Value('VALUE_B'))

對於在協定訊息中宣告的列舉 (例如上面的 Foo),語法類似

self.assertEqual('VALUE_A', myproto_pb2.Foo.SomeEnum.Name(myproto_pb2.Foo.VALUE_A))
self.assertEqual(5, myproto_pb2.Foo.SomeEnum.Value('VALUE_B'))

如果多個列舉常數具有相同的值 (別名),則傳回定義的第一個常數。

enum SomeEnum {
    option allow_alias = true;
    VALUE_A = 0;
    VALUE_B = 5;
    VALUE_C = 1234;
    VALUE_B_ALIAS = 5;
}

在上面的範例中,myproto_pb2.SomeEnum.Name(5) 傳回 "VALUE_B"

Oneof

給定具有 oneof 的訊息

message Foo {
  oneof test_oneof {
     string name = 1;
     int32 serial_number = 2;
  }
}

對應於 Foo 的 Python 類別將具有名為 nameserial_number 的屬性,就像一般的 欄位 一樣。但是,與一般欄位不同,oneof 中的欄位一次最多只能設定一個,這是由執行階段確保的。例如

message = Foo()
message.name = "Bender"
assert message.HasField("name")
message.serial_number = 2716057
assert message.HasField("serial_number")
assert not message.HasField("name")

訊息類別也有一個 WhichOneof 方法,可讓您找出 oneof 中的哪個欄位 (如果有的話) 已設定。此方法會傳回已設定欄位的名稱,如果沒有設定任何欄位,則傳回 None

assert message.WhichOneof("test_oneof") is None
message.name = "Bender"
assert message.WhichOneof("test_oneof") == "name"

HasFieldClearField 除了欄位名稱之外,也接受 oneof 名稱

assert not message.HasField("test_oneof")
message.name = "Bender"
assert message.HasField("test_oneof")
message.serial_number = 2716057
assert message.HasField("test_oneof")
message.ClearField("test_oneof")
assert not message.HasField("test_oneof")
assert not message.HasField("serial_number")

請注意,在 oneof 上呼叫 ClearField 只會清除目前設定的欄位。

與 Python 關鍵字衝突的名稱

如果訊息、欄位、列舉或列舉值的名稱是 Python 關鍵字,則其對應的類別或屬性的名稱將相同,但您只能使用 Python 的 getattr()setattr() 內建函式來存取它,而不能透過 Python 的正常屬性參考語法 (即點運算子)。

例如,如果您有以下 .proto 定義

message Baz {
  optional int32 from = 1
  repeated int32 in = 2;
}

您將像這樣存取這些欄位

baz = Baz()
setattr(baz, "from", 99)
assert getattr(baz, "from") == 99
getattr(baz, "in").append(42)
assert getattr(baz, "in") == [42]

相反地,嘗試使用 obj.attr 語法來存取這些欄位會導致 Python 在剖析您的程式碼時引發語法錯誤

# WRONG!
baz.in  # SyntaxError: invalid syntax
baz.from  # SyntaxError: invalid syntax

擴充功能 (僅限 proto2)

給定具有擴充範圍的訊息

message Foo {
  extensions 100 to 199;
}

對應於 Foo 的 Python 類別將具有一個名為 Extensions 的成員,它是一個字典,將擴充識別符對應到它們目前的值。

給定一個擴充定義

extend Foo {
  optional int32 bar = 123;
}

協定緩衝區編譯器會產生一個名為 bar 的「擴充識別符」。識別符充當 Extensions 字典的索引鍵。在此字典中查閱值的結果與您存取相同類型的正常欄位完全相同。因此,給定上面的範例,您可以執行

foo = Foo()
foo.Extensions[proto_file_pb2.bar] = 2
assert foo.Extensions[proto_file_pb2.bar] == 2

請注意,您需要指定擴充識別符常數,而不僅僅是字串名稱:這是因為在不同的範圍中可能會指定多個具有相同名稱的擴充功能。

與一般欄位類似,Extensions[...] 傳回單數訊息的訊息物件和重複欄位的序列。

Message 介面的 HasField()ClearField() 方法不適用於擴充功能;您必須改用 HasExtension()ClearExtension()。若要使用 HasExtension()ClearExtension() 方法,請傳入您要檢查其存在的擴充功能的 field_descriptor

服務

如果 .proto 檔案包含以下行

option py_generic_services = true;

那麼協定緩衝區編譯器將根據檔案中找到的服務定義產生程式碼,如本節所述。但是,產生的程式碼可能是不需要的,因為它未與任何特定的 RPC 系統綁定,因此需要比針對一個系統量身定制的程式碼更多的間接層級。如果您*不*想要產生此程式碼,請將此行新增至檔案

option py_generic_services = false;

如果未給出以上任何一行,則選項預設為 false,因為通用服務已棄用。(請注意,在 2.4.0 之前的版本中,選項預設為 true)

基於 .proto 語言服務定義的 RPC 系統應提供 外掛程式,以產生適合系統的程式碼。這些外掛程式可能需要停用抽象服務,以便它們可以產生具有相同名稱的自己的類別。外掛程式是 2.3.0 版 (2010 年 1 月) 中的新增功能。

本節的其餘部分描述了在啟用抽象服務時協定緩衝區編譯器產生的內容。

介面

給定一個服務定義

service Foo {
  rpc Bar(FooRequest) returns(FooResponse);
}

協定緩衝區編譯器將產生一個類別 Foo 來表示此服務。Foo 將為服務定義中定義的每個方法提供一個方法。在這種情況下,方法 Bar 定義為

def Bar(self, rpc_controller, request, done)

參數等效於 Service.CallMethod() 的參數,但 method_descriptor 引數是隱含的。

這些產生的方法旨在由子類別覆寫。預設實作僅呼叫 controller.SetFailed(),並顯示錯誤訊息,指出該方法未實作,然後調用 done 回呼。當實作您自己的服務時,您必須子類別化此產生的服務並根據需要實作其方法。

Foo 繼承自 Service 介面。協定緩衝區編譯器會自動產生 Service 方法的實作,如下所示

  • GetDescriptor:傳回服務的 ServiceDescriptor
  • CallMethod:根據提供的方法描述符判斷要呼叫的方法,並直接呼叫它。
  • GetRequestClassGetResponseClass:傳回給定方法的正確類型之請求或回應的類別。

Stub

協定緩衝區編譯器還為每個服務介面產生一個「stub」實作,供希望向實作服務的伺服器傳送請求的用戶端使用。對於 Foo 服務 (如上所示),將定義 stub 實作 Foo_Stub

Foo_StubFoo 的子類別。其建構函式採用 RpcChannel 作為參數。然後,stub 透過呼叫通道的 CallMethod() 方法來實作每個服務的方法。

Protocol Buffer 程式庫不包含 RPC 實作。但是,它包含將產生的服務類別連接到您選擇的任何任意 RPC 實作所需的所有工具。您只需要提供 RpcChannelRpcController 的實作。

外掛程式插入點

程式碼產生器外掛程式,想要擴充 Python 程式碼產生器的輸出,可以使用給定的插入點名稱插入以下類型的程式碼。

  • imports:匯入陳述式。
  • module_scope:最上層宣告。

在 Python 和 C++ 之間共用訊息

在 Protobuf Python API 的 4.21.0 版本之前,Python 應用程式可以使用原生擴充功能與 C++ 共用訊息。從 4.21.0 API 版本開始,預設安裝不再支援在 Python 和 C++ 之間共用訊息。若要在使用 4.x 及更高版本的 Protobuf Python API 時啟用此功能,請定義環境變數 PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=cpp,並確保已安裝 Python/C++ 擴充功能。