Python 生成代码指南

精确描述了 protocol buffer 编译器为任何给定协议定义生成的 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.protosrc/bar/baz.proto 并生成两个输出文件:build/gen/foo_pb2.pybuild/gen/bar/baz_pb2.py。编译器将根据需要自动创建目录 build/gen/bar,但不会创建 buildbuild/gen;它们必须已经存在。

Protoc 可以使用 --pyi_out 参数生成 Python stubs (.pyi)。

请注意,如果 .proto 文件或其路径包含任何不能在 Python 模块名中使用的字符(例如,连字符),它们将被下划线替换。因此,文件 foo-bar.proto 变成 Python 文件 foo_bar_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 string) 或从该格式转换。例如

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 string 和其他时间单位之间进行转换。要在 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 string 或从该格式转换。此外,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")

如果您需要一个没有任何可设置或想设置的字段的消息上的“已设置”位,您可以使用 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() 方法会附加整个消息列表,但会创建列表中每个消息的副本

例如,给定此消息定义

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)

请注意,groups 已弃用,创建新的消息类型时不应使用它们——请改用嵌套消息类型。

group 将嵌套消息类型和字段组合成一个声明,并对消息使用不同的线格式。生成的消与 group 同名。生成的字段名是 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 可以是 requiredoptionalrepeated。必需或可选的 group 使用与普通单一消息字段相同的 API 进行操作。重复的 group 使用与普通重复消息字段相同的 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 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;
  }
  optional 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 关键字冲突的名称一节所述。

您可以在枚举中设置的值取决于您的 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'))

对于在 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 类将具有名为 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)

给定一个带有扩展范围的消息

message Foo {
  extensions 100 to 199;
}

Foo 相对应的 Python 类将有一个名为 Extensions 的成员,这是一个将扩展标识符映射到其当前值的字典。

给定一个扩展定义

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

服务

如果 .proto 文件包含以下行

option py_generic_services = true;

然后 protocol buffer 编译器将根据文件中找到的服务定义生成本节所述的代码。然而,生成的代码可能不尽人意,因为它没有绑定到任何特定的 RPC 系统,因此比针对特定系统量身定制的代码需要更多的间接层。如果您不希望生成此代码,请在文件中添加此行

option py_generic_services = false;

如果上面两行都没有给出,则该选项默认为 false,因为通用服务已弃用。(请注意,在 2.4.0 之前,该选项默认为 true

基于 .proto 语言服务定义的 RPC 系统应该提供 插件 来生成适用于该系统的代码。这些插件可能需要禁用抽象服务,以便它们可以生成同名的自己的类。插件是 2.3.0 版本(2010 年 1 月)中新增的。

本节的其余部分描述了当启用抽象服务时,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: 根据提供的方法描述符确定要调用的方法并直接调用它。
  • GetRequestClassGetResponseClass: 返回给定方法对应的正确类型的请求或响应的类。

Stub

protocol buffer 编译器还会为每个服务接口生成一个“stub”实现,供希望向实现该服务的服务器发送请求的客户端使用。对于上面的 Foo 服务,将定义 stub 实现 Foo_Stub

Foo_StubFoo 的子类。其构造函数将 RpcChannel 作为参数。然后,该 stub 通过调用通道的 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++ 扩展。