Python 生成代码指南

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

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

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

众所周知的类型

协议缓冲区提供了许多 众所周知的类型,您可以在 .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 对象和 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):将两个 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")

重复字段

重复字段有三种类型:标量、枚举和消息。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)

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;
}

组可以是 requiredoptionalrepeated。required 或 optional 组使用与常规单数消息字段相同的 API 进行操作。repeated 组使用与常规重复消息字段相同的 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 中创建一个键,其值为零/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_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 关键字冲突的名称 部分所述。

您可以在枚举中设置的值取决于您的协议缓冲区版本

  • 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 类将具有名为 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;
}

协议缓冲区编译器生成一个名为 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:根据提供的方法描述符确定要调用的方法并直接调用它。
  • GetRequestClassGetResponseClass:返回给定方法的正确类型的请求或响应类。

存根

协议缓冲区编译器还为每个服务接口生成一个“存根”实现,客户端希望使用该实现向实现服务的服务器发送请求。对于 Foo 服务(如上),将定义存根实现 Foo_Stub

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