Python 生成程式碼指南

精確描述了協議緩衝區編譯器為任何給定的協議定義生成的 Python 定義。

此文件中強調了 proto2、proto3 和 Editions 生成程式碼之間的任何差異——請注意,這些差異存在於本文件中描述的生成程式碼中,而不是基礎訊息類/介面中,它們在所有版本中都是相同的。在閱讀本文件之前,您應該閱讀proto2 語言指南proto3 語言指南和/或Editions 指南

Python Protocol Buffers 的實現與 C++ 和 Java 略有不同。在 Python 中,編譯器僅輸出用於構建生成類的描述符的程式碼,而Python 元類完成實際工作。本文件描述了元類應用後您獲得的內容。

編譯器呼叫

當使用 --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

包(Packages)

協議緩衝區編譯器生成的 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

知名型別

協議緩衝區提供了許多知名型別,您可以在您的 .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 物件和 Timestamps 之間進行轉換。例如

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

要獲取或建立列表/結構,您可以呼叫 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。例如,給定欄位 int32 foo_bar = 5;,編譯器將生成常量 FOO_BAR_FIELD_NUMBER = 5

如果欄位的名稱是 Python 關鍵字,則其屬性只能透過 getattr()setattr() 訪問,如與 Python 關鍵詞衝突的名稱部分所述。

協議緩衝區定義了兩種欄位存在模式:explicitimplicit。以下各節將分別描述它們。

具有顯式存在性的單一欄位

具有 explicit 存在的單一欄位總是能夠區分欄位未設定和欄位設定為其預設值的情況。

如果您有任何非訊息型別的單一欄位 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")

在 Editions 中,欄位預設具有 explicit 存在。以下是 Editions .proto 檔案中 explicit 欄位的示例

edition = "2023";
message MyMessage {
  int32 foo = 1;
}

具有隱式存在性的單一欄位

具有 implicit 存在的單一欄位沒有 HasField() 方法。implicit 欄位始終“已設定”,讀取該欄位將始終返回值。讀取未賦值的 implicit 欄位將返回該型別的預設值。

如果您有任何非訊息型別的單一欄位 foo,您可以像操作常規欄位一樣操作欄位 foo。例如,如果 foo 的型別是 int32,您可以說

message.foo = 123
print(message.foo)

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

如果 foo 在未設定時被讀取,其值是該欄位的預設值。要清除 foo 的值並將其重置為該型別的預設值,您需要呼叫 Message 介面的 ClearField() 方法。例如

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

奇異訊息欄位

訊息型別的工作方式略有不同。您不能為嵌入式訊息欄位賦值。相反,為子訊息中的任何欄位賦值意味著在父訊息中設定該訊息欄位。子訊息始終具有顯式存在,因此您還可以使用父訊息的 HasField() 方法來檢查訊息型別欄位值是否已設定。

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

edition = "2023";
message Foo {
  Bar bar = 1;
}
message Bar {
  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")

重複欄位

重複欄位有三種類型:標量、列舉和訊息。對映欄位和 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() 方法會附加整個訊息列表,但會複製列表中的每條訊息。

例如,給定此訊息定義

edition = "2023";
message Foo {
  repeated Bar bars = 1;
}
message Bar {
  int32 i = 1;
  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)

請注意,組已被棄用,在建立新訊息型別時不應使用——請改用巢狀訊息型別(proto2、proto3)或帶分隔符的欄位(editions)。

組將巢狀訊息型別和欄位組合成一個宣告,併為訊息使用不同的線格式。生成的郵件與組同名。生成的欄位名是組名的小寫形式。

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

// 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。必需或可選組使用與常規單一訊息欄位相同的 API 進行操作。重複組使用與常規重複訊息欄位相同的 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")

對映欄位

給定此訊息定義

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

對映欄位生成的 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 中建立該鍵,並賦予其零/假/空值。這種行為更類似於 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;
  }
  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 關鍵詞衝突的名稱一節所述。

對於 proto2,列舉是封閉的,對於 proto3,列舉是開放的。在 Editions 中,enum_type 特性決定了列舉的行為。

  • OPEN 列舉可以具有任何 int32 值,即使該值未在列舉定義中指定。這是 Editions 中的預設行為。
  • CLOSED 列舉不能包含除列舉型別定義的值之外的數字值。如果您分配一個不在列舉中的值,生成的程式碼將丟擲異常。這等同於 proto2 中列舉的行為。

列舉具有許多實用方法,用於從值獲取欄位名稱以及反之,欄位列表等等——這些方法定義在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 或 editions 訊息

edition = "2023";
message Foo {
  extensions 100 to 199;
}

對應於 Foo 的 Python 類將有一個名為 Extensions 的成員,它是一個將擴充套件識別符號對映到其當前值的字典。

給定一個擴充套件定義:

extend Foo {
  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

服務(Services)

如果 .proto 檔案包含以下行:

option py_generic_services = true;

然後協議緩衝區編譯器將根據檔案中找到的服務定義生成程式碼,如本節所述。但是,生成的程式碼可能不理想,因為它不與任何特定的 RPC 系統繫結,因此需要更多的間接層,而不是針對一個系統的程式碼。如果您不希望生成此程式碼,請將此行新增到檔案中

option py_generic_services = false;

如果上述兩行都未給出,該選項預設為 false,因為通用服務已被棄用。(請注意,在 2.4.0 之前,該選項預設為 true

基於 .proto 語言服務定義的 RPC 系統應提供外掛來生成適合該系統的程式碼。這些外掛很可能要求停用抽象服務,以便它們可以生成自己同名的類。

本節的其餘部分描述了當啟用抽象服務時,Protocol Buffer 編譯器會生成什麼。

介面

給定一個服務定義:

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

協議緩衝區編譯器將生成一個名為 Foo 的類來表示此服務。Foo 將具有服務定義中定義的每個方法的方法。在此示例中,方法 Bar 定義如下

def Bar(self, rpc_controller, request, done)

引數與 Service.CallMethod() 的引數等效,只是 method_descriptor 引數是隱式的。

這些生成的方法旨在被子類重寫。預設實現只是呼叫 controller.SetFailed() 並附帶一條指示方法未實現的錯誤訊息,然後呼叫 done 回撥。在實現您自己的服務時,您必須子類化此生成的服務並根據需要實現其方法。

FooService 介面的子類。Protocol Buffer 編譯器會自動生成 Service 方法的實現,如下所示:

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

存根

協議緩衝區編譯器還會為每個服務介面生成一個“存根”實現,客戶端希望向實現該服務的伺服器傳送請求時會使用該實現。對於 Foo 服務(如上),將定義存根實現 Foo_Stub

Foo_StubFoo 的子類。它的建構函式接受一個 RpcChannel 作為引數。然後存根透過呼叫通道的 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++ 擴充套件。