Python 生成代码指南
突出显示了 proto2 和 proto3 生成代码之间的任何差异 - 请注意,这些差异存在于本文档中描述的生成代码中,而不是基本消息类/接口中,这两个版本中的基本消息类/接口是相同的。在阅读本文档之前,您应该阅读proto2 语言指南和/或proto3 语言指南。
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.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 代码时,协议缓冲区编译器直接输出到 ZIP 存档的功能特别方便,因为如果将 Python 解释器放置在PYTHONPATH
中,则它能够直接读取这些存档。要输出到 ZIP 文件,只需提供一个以.zip
结尾的输出位置。注意
扩展名_pb2.py
中的数字 2 表示 Protocol Buffers 的版本 2。版本 1 主要在 Google 内部使用,尽管您可能能够在其他 Python 代码中找到其部分内容,这些代码是在 Protocol Buffers 之前发布的。由于 Python Protocol Buffers 的版本 2 具有完全不同的接口,并且由于 Python 没有编译时类型检查来捕获错误,因此我们选择使版本号成为生成 Python 文件名的突出部分。目前,proto2 和 proto3 都使用_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()
要转换自纪元以来的其他时间单位,您可以调用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
。例如,给定字段 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")
映射字段
给定此消息定义
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]
与嵌入式消息字段一样,消息不能直接分配到映射值中。相反,要将消息作为映射值添加,您可以引用未定义的键,它会构造并返回一个新的子消息
m.message_map[key].submessage_field = 10
您可以在下一节中找到有关未定义键的更多信息。
引用未定义的键
在处理未定义键时,Protocol Buffer 映射的语义与 Python dict
略有不同。在常规 Python dict
中,引用未定义的键会引发 KeyError 异常
>>> x = {}
>>> x[5]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 5
但是,在 Protocol Buffers 映射中,引用未定义的键会在映射中创建该键,并使用零/假/空值。此行为更类似于 Python 标准库 defaultdict
。
>>> dict(m.mapfield)
{}
>>> m.mapfield[5]
0
>>> dict(m.mapfield)
{5: 0}
此行为对于具有消息类型值的映射特别方便,因为您可以直接更新返回的消息的字段。
>>> m.message_map[5].foo = 3
请注意,即使您没有为消息字段分配任何值,子消息仍会在映射中创建
>>> 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 关键字冲突的名称部分所述。
您可以设置的枚举值取决于您的协议缓冲区版本
- 在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
给定一个带有一个of的消息
message Foo {
oneof test_oneof {
string name = 1;
int32 serial_number = 2;
}
}
对应于 Foo
的 Python 类将具有名为 name
和 serial_number
的属性,就像常规字段一样。但是,与常规字段不同,一次最多只能设置一个of中的一个字段,这是由运行时确保的。例如
message = Foo()
message.name = "Bender"
assert message.HasField("name")
message.serial_number = 2716057
assert message.HasField("serial_number")
assert not message.HasField("name")
消息类还有一个 WhichOneof
方法,允许您找出一个of中哪个字段(如果有)已设置。此方法返回已设置的字段的名称,如果未设置任何内容,则返回 None
assert message.WhichOneof("test_oneof") is None
message.name = "Bender"
assert message.WhichOneof("test_oneof") == "name"
HasField
和 ClearField
除了字段名称之外还接受一个of名称
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")
请注意,对一个of调用 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
:根据提供的 method descriptor 确定正在调用的方法,并直接调用它。GetRequestClass
和GetResponseClass
:返回给定方法的正确类型的请求或响应的类。
存根
协议缓冲区编译器还会生成每个服务接口的“存根”实现,客户端希望向实现该服务的服务器发送请求时会使用它。对于Foo
服务(如上),将定义存根实现Foo_Stub
。
Foo_Stub
是Foo
的子类。它的构造函数将RpcChannel
作为参数。然后,存根通过调用通道的CallMethod()
方法来实现服务的每个方法。
Protocol Buffer 库不包含 RPC 实现。但是,它包含将生成的 service 类连接到您选择的任何任意 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++ 扩展。