Python 程式碼生成指南
proto2 和 proto3 生成的程式碼之間的任何差異都會被突出顯示 - 請注意,這些差異是在本文檔中描述的生成程式碼中,而不是在基本訊息類別/介面中,它們在兩個版本中是相同的。在閱讀本文檔之前,您應該閱讀proto2 語言指南和/或proto3 語言指南。
Python Protocol Buffers 實作與 C++ 和 Java 有點不同。在 Python 中,編譯器只會輸出程式碼來建構生成類別的描述符,而 Python 元類別會執行真正的工作。本文檔描述在應用元類別之後您會得到的東西。
編譯器調用
當使用 --python_out=
命令列標記調用時,protocol buffer 編譯器會產生 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.proto
和 src/bar/baz.proto
,並產生兩個輸出檔案:build/gen/foo_pb2.py
和 build/gen/bar/baz_pb2.py
。編譯器會在必要時自動建立目錄 build/gen/bar
,但它不會建立 build
或 build/gen
;它們必須已經存在。
Protoc 可以使用 --pyi_out
參數生成 Python 存根 (.pyi
)。
請注意,如果 .proto
檔案或其路徑包含任何無法在 Python 模組名稱中使用的字元(例如,連字號),它們將被替換為底線。因此,檔案 foo-bar.proto
會變成 Python 檔案 foo_bar_pb2.py
。
提示
在輸出 Python 程式碼時,protocol buffer 編譯器直接輸出到 ZIP 封存檔的能力特別方便,因為如果放置在PYTHONPATH
中,Python 直譯器可以直接從這些封存檔讀取。若要輸出到 ZIP 檔案,只需提供以 .zip
結尾的輸出位置。注意
副檔名_pb2.py
中的數字 2 表示 Protocol Buffers 的第 2 版。第 1 版主要在 Google 內部使用,不過您可能會在 Protocol Buffers 之前發佈的其他 Python 程式碼中找到它的一部分。由於 Python Protocol Buffers 的第 2 版具有完全不同的介面,而且由於 Python 沒有編譯時期類型檢查來捕捉錯誤,我們選擇讓版本號碼成為生成 Python 檔案名稱的重要部分。目前,proto2 和 proto3 都使用 _pb2.py
作為其生成的檔案。套件
protocol buffer 編譯器生成的 Python 程式碼完全不受 .proto
檔案中定義的套件名稱的影響。相反地,Python 套件是透過目錄結構來識別的。
訊息
給定一個簡單的訊息宣告
message Foo {}
protocol buffer 編譯器會生成一個名為 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 訊息除了常見的 protocol buffer 訊息方法外,還具有特殊方法,因為它們既是 google.protobuf.Message
的子類別,也是 WKT 類別的子類別。
Any
對於 Any 訊息,您可以呼叫 Pack()
將指定的訊息封裝到目前的 Any 訊息中,或呼叫 Unpack()
將目前的 Any 訊息解封裝到指定的訊息中。例如
any_message.Pack(message)
any_message.Unpack(message)
Unpack()
還會針對儲存的訊息檢查傳遞的訊息物件的描述符,如果它們不匹配,則會傳回 False
,並且不會嘗試解封裝;否則傳回 True
。
您也可以呼叫 Is()
方法來檢查 Any 訊息是否代表給定的 protocol buffer 類型。例如
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)
:將兩個 FieldMasks 取交集並存入此 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
要取得或建立 list/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)
如果您有一個單數(可選或必填)且類型為非訊息的欄位 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")
重複欄位
重複欄位表示為一個類似 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;
}
群組可以是 required
、optional
或 repeated
。必填或可選群組使用與一般單數訊息欄位相同的 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")
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 Buffer 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_A
、VALUE_B
和 VALUE_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 類別將具有名為 name
和 serial_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"
HasField
和 ClearField
除了欄位名稱之外,還接受 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
:根據提供的方法描述符判斷正在呼叫哪個方法,並直接呼叫它。GetRequestClass
和GetResponseClass
:傳回給定方法的正確型別的要求或回應的類別。
Stub
協議緩衝區編譯器也會產生每個服務介面的「存根」實作,供希望向實作該服務的伺服器傳送要求的用戶端使用。對於 Foo
服務(如上所述),將會定義存根實作 Foo_Stub
。
Foo_Stub
是 Foo
的子類別。它的建構函式會採用 RpcChannel
作為參數。然後,存根會透過呼叫通道的 CallMethod()
方法來實作服務的每個方法。
協議緩衝區程式庫不包含 RPC 實作。然而,它包含您需要的所有工具,可將產生的服務類別連結到您選擇的任何任意 RPC 實作。您只需要提供 RpcChannel
和 RpcController
的實作。
外掛插入點
想要擴充 Python 程式碼產生器輸出的 程式碼產生器外掛程式可以使用指定的插入點名稱插入以下類型的程式碼。
imports
:import 語句。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++ 擴充功能。