Python 生成代码指南
proto2、proto3 和 Editions 生成代码之间的任何差异都会被高亮显示——请注意,这些差异存在于本文档所描述的生成代码中,而不是在基础消息类/接口中,后者在所有版本中都是相同的。在阅读本文档之前,您应该阅读 proto2 语言指南、proto3 语言指南 和/或 Editions 指南。
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 和 Editions 都为其生成的文件使用 _pb2.py。包(Packages)
由 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 模块来处理文本格式的 protocol 消息:例如,Merge() 方法允许您将消息的 ASCII 表示合并到一个现有消息中。
嵌套类型
消息可以声明在另一个消息内部。例如:
message Foo {
message Bar {}
}
在这种情况下,Bar 类被声明为 Foo 的一个静态成员,因此您可以将其称为 Foo.Bar。
知名类型
Protocol buffers 提供了一些知名类型 (WKT),您可以在您的 .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()
要在其他时间单位(自纪元以来)之间进行转换,您可以调用 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 对于 Message Descriptor 是否有效。AllFieldsFromDescriptor(message_descriptor): 将 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 关键字冲突的名称部分。
Protocol buffers 定义了两种字段存在模式:explicit(显式)和 implicit(隐式)。以下各节将分别描述这两种模式。
具有显式存在性的单一字段
具有 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")
重复字段
有三种类型的重复字段:标量、枚举和消息。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)
除了使用 Python 的 del 外,Message 接口的 ClearField() 方法也同样有效。
当使用索引检索值时,您可以使用负数,例如使用 -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)
请注意,群组(groups)已被弃用,在创建新的消息类型时不应使用——请改用嵌套消息类型(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;
}
群组可以是 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")
映射字段
给定此消息定义:
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;
}
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 关键字冲突的名称部分。
在 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'))
对于在 protocol 消息中声明的枚举,例如上面的 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 或 editions 消息:
edition = "2023";
message Foo {
extensions 100 to 199;
}
与 Foo 对应的 Python 类将有一个名为 Extensions 的成员,它是一个将扩展标识符映射到其当前值的字典。
给定一个扩展定义:
extend Foo {
int32 bar = 123;
}
protocol buffer 编译器会生成一个名为 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;
然后,protocol buffer 编译器将根据文件中找到的服务定义生成代码,如本节所述。但是,生成的代码可能不理想,因为它不与任何特定的 RPC 系统绑定,因此需要比为某个系统量身定制的代码更多的间接层级。如果您不希望生成此代码,请将此行添加到文件中:
option py_generic_services = false;
如果上述两行都未给出,该选项默认为 false,因为通用服务已被弃用。(请注意,在 2.4.0 之前,该选项默认为 true)
基于 .proto 语言服务定义的 RPC 系统应提供插件来生成适合该系统的代码。这些插件很可能要求禁用抽象服务,以便它们可以生成自己同名的类。
本节的其余部分描述了当启用抽象服务时,Protocol Buffer 编译器会生成什么。
接口
给定一个服务定义:
service Foo {
rpc Bar(FooRequest) returns(FooResponse);
}
protocol buffer 编译器将生成一个类 Foo 来表示此服务。Foo 将为服务定义中定义的每个方法提供一个方法。在这种情况下,方法 Bar 定义为:
def Bar(self, rpc_controller, request, done)
这些参数等同于 Service.CallMethod() 的参数,只是 method_descriptor 参数是隐含的。
这些生成的方法旨在由子类重写。默认实现只是使用一个指示方法未实现错误消息调用 controller.SetFailed(),然后调用 done 回调。在实现您自己的服务时,您必须子类化这个生成的服务并适当地实现其方法。
Foo 是 Service 接口的子类。Protocol Buffer 编译器会自动生成 Service 方法的实现,如下所示:
GetDescriptor: 返回服务的ServiceDescriptor。CallMethod: 根据提供的方法描述符确定正在调用哪个方法,并直接调用它。GetRequestClass和GetResponseClass: 返回给定方法的正确类型的请求或响应的类。
存根
protocol buffer 编译器还会为每个服务接口生成一个“存根”实现,供希望向实现该服务的服务器发送请求的客户端使用。对于 Foo 服务(如上),将定义存根实现 Foo_Stub。
Foo_Stub 是 Foo 的子类。其构造函数接受一个 RpcChannel 作为参数。然后,该存根通过调用通道的 CallMethod() 方法来实现每个服务的方法。
Protocol Buffer 库不包含 RPC 实现。但是,它包含了将生成的服务类连接到您选择的任何任意 RPC 实现所需的所有工具。您只需提供 RpcChannel 和 RpcController 的实现。
插件插入点
希望扩展 Python 代码生成器输出的代码生成器插件可以使用给定的插入点名称插入以下类型的代码。
imports: 导入语句。module_scope: 顶层声明。
警告
不要生成依赖于标准代码生成器声明的私有类成员的代码,因为这些实现细节在未来版本的 Protocol Buffers 中可能会改变。在 Python 和 C++ 之间共享消息
在 Protobuf Python API 的 4.21.0 版本之前,Python 应用可以使用本机扩展与 C++ 共享消息。从 4.21.0 API 版本开始,默认安装不支持在 Python 和 C++ 之间共享消息。要在 Protobuf Python API 的 4.x 及更高版本中启用此功能,请定义环境变量 PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=cpp,并确保已安装 Python/C++ 扩展。