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
子类。生成的类不是为子类化设计的,可能会导致“脆弱基类”问题。此外,实现继承是一种糟糕的设计。
除了 Message
接口定义的成员以及为嵌套字段、消息和枚举类型(如下所述)生成的成员之外,Python 消息类没有其他特定的公共成员。Message
提供了可以用来检查、操作、读取或写入整个消息的方法,包括从二进制字符串解析和序列化到二进制字符串。除了这些方法,Foo
类还定义了以下静态方法:
FromString(s)
:返回一个从给定字符串反序列化出的新消息实例。
请注意,您还可以使用 text_format
模块来处理文本格式的协议消息:例如,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 对象和 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 对 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)。
一个组(group)将一个嵌套消息类型和一个字段合并为一个声明,并为该消息使用不同的线路格式。生成的消息与该组同名。生成的字段名称是该组名称的**小写**形式。
例如,除了线路格式外,以下两个消息定义是等效的:
// 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;
}
一个组(group)可以是 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 maps 的语义与 Python `dict` 的行为略有不同。在常规的 Python `dict` 中,引用一个未定义的键会引发一个 KeyError 异常:
>>> x = {}
>>> x[5]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 5
然而,在 Protocol Buffers maps 中,引用一个未定义的键会在 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'))
对于在协议消息中声明的枚举,例如上面的 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++ 之间共享消息。要在 4.x 及更高版本的 Protobuf Python API 中启用此功能,请定义环境变量 PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=cpp
,并确保已安装 Python/C++ 扩展。