Protocol Buffers python生成代码 翻译自https://developers.google.com/protocol-buffers/docs/reference/python-generated

本文描述的是protocol buffer编译器生成的各种protocol定义在python中是如何定义的。proto2 和 proto3生成代码中的任何不同点都会高亮标识,但是最基本的message类/接口在这两个版本中是没有区别的。在阅读本文之前,你最好先阅读proto2 language guide和/或proto3 language guide

Python版的Protocol Buffers 在实现上和C++及Java有小许的不同。在Python中,编译器只是输出了用于构建生成类描述的代码,实际起作用的是Python元类。如果理解了Python元类,读这篇文章可能更好。

Compiler Invocation

当在调用protocol buffer compiler命令时加上 --python_out= 标识就会产生Python版protocol输出。--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 ,所以必须保证这两个文件存在存在。

需要注意的一点是,如果 .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内部使用,尽管如此,外部还是有一些Python代码中包含了版本1的代码。因为Python Protocol Buffer版本2的接口有很大不同,而且Python没有编译时检查来捕获因为版本变化而产生的错误。因此,我们选择将版本号作为Python文件名的一部分来进行区分。目前来说,proto2 和 proto3 的生成文件都使用 _pb2.py。

Packages

由protocol buffer编译器产生的python代码完全不受 .proto文件中的 package 名影响。Python的包是通过文件结构定义的。

Messages

给一个简单的mesage定义:

message Foo {}

protocol buffer编译器会生成一个名为 Foo 的类,该类是 google.protobuf.Message 的子类。 该类是一个具体类,没有未实现的抽象方法。不同于C++ 和 Java,Python生成码不受 .proto 文件中的 optimize_for 选项影响,实际上所有Python代码会对代码大小进行优化。

如果message名字是Python的关键字,那么该类只能通过 getattr() 进行获取。具体在 Names which conflict with Python keywords 章节会有具体描述。

你不能创建Foo的之类。生成的类在设计上没有考虑之类,且会造成“脆弱的基类”问题(fragile base class)。而且,实现继承(implementation inheritance)是一种不好的设计。

Python message类没有特别的public成员,除了那些通过Message接口 定义和那些嵌套字段、message和枚举类型(下面会具体描述)。Message提供了进行检查、操控、读和写整个message的方法,包括了序列化到字符串和从字符串反序列化的方法。除了上述这些方法,Foo类还定义了如下的静态方法:

  • FromString(s):对给定字符串进行反序列化,斌返回一个新的message实例。

你也可以使用text_format方法对text格式的protocol message进行处理:例如,你可以使用 Merge() 方法将ASCII形式的message信息,整合到一个存在的message中。

message中可以定义另一个message。例如,message Foo { message Bar { } }

这这个例子中,Bar类被定义为Foo的一个静态成员,因此你可以通过 Foo.Bar 进行访问。

Well Known Types

Protocol buffers提供了一些(常见类型)[https://developers.google.com/protocol-buffers/docs/reference/google.protobuf] 的message,你可以在你自己定义的message类型中使用他们。一些常见类型(WKT)message除了拥有普通protocol buffer message的方法之外,还有一些特殊的方法,因为其是google.protobuf.Message 和WKT的子类。

Any

对于Any message(泛型), 你可以使用 Pack() 将特定的message转化为 Any message,或通过 Unpack() 方法将当前Any message转为特定的message。例如:

any_message.Pack(message) 
any_message.Unpack(message)

你也可以使用 Is() 方法来判断Any message是否为特定的protocol buffer类型。例如:

assert any_message.Is(message.DESCRIPTOR)

Timestamp

可以通过 ToJsonString()/FromJsonString() 方法实现 对Timestamp message(时间戳)和RFC 3339格式数据(Json字符串) 的相互转化。例如:

timestamp_message.FromJsonString("1970-01-01T00:00:00Z")
assert timestamp_message.ToJsonString() == "1970-01-01T00:00:00Z"

你也可以调用 GetCurrentTime() 方法 对 对Timestamp message赋值当前时间:

timestamp_message.GetCurrentTime()

若要转化不同精度的时间,可以使用以下方法 ToNanoseconds() FromNanoseconds() ToMicroseconds() FromMicroseconds() ToMilliseconds() FromMilliseconds() ToSeconds() FromSeconds()。生成代码还提供了 ToDatetime()FromDatetime() 方法来和Python的datetime对象进行相互转化。例如:

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 messages (持续时间) 用和对Timestamp message相同的方法来和JSON字符串进行相互转化。 你可以使用 ToTimedelta()FromTimedelta 来对 timedelta 和 Duration进行相互转化,例如:

duration_message.FromNanoseconds(1999999999)
td = duration_message.ToTimedelta()
assert td.seconds == 1
assert td.microseconds == 999999

FieldMask

FieldMask messages 和JSON字符串通过 ToJsonString()/FromJsonString() 进行相互转化。除此之外,FieldMask message还提供了如下方法:

  • IsValidForDescriptor::检查FieldMask是否为合法的Message 描述符(Descriptor)。
  • AllFieldsFromDescriptor:获取Message描述符的全部直接字段。
  • CanonicalFormFromMask:将FieldMask转为规范形式。
  • Union:将两个FieldMask的并集放到到当前FieldMask。
  • Intersect:将两个FieldMask的交集放到当前FieldMask。
  • MergeMessage:将FieldMask中指定字段从源整合到目标位置。

Struct

Struct message 使你可以直接获取和设置item,例如:

struct_message["key1"] = 5
struct_message["key2"] = "abc"
struct_message["key3"] = True

要获取和创建一个 list/struct,你可以使用 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 message和 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

调用 add_list()/add_struct() 来添加一个ListValue/Struct。例如:

list_value.add_struct()["key"] = 1
list_value.add_list().extend([1, "two", True])

Fields

对于message中的每个字段,message对应的python类中会有相同名字的属性。如何操作这些字段,取决于他的类型。 和属性一样,编译器为每个字段序号也生成了一个整形常量。常量的命名是字段名转化为大写,再追加 _FIELD_NUMBER。例如,字段 optional int32 foo_bar = 5, 编译器会生成 FOO_BAR_FIELD_NUMBER = 5。 如果字段名是Python的关键字,那么该属性,只能通过 getattr()setattr() 来进行访问。Names which conflict with Python keywords一节会对这些冲突的关键字进行描述。

Singular Fields (proto2)

假设你有一个非message类型的单一字段(optional 或 required) foo,如果其类型是 int32, 你可以用如下方法对其进行操作:

message.foo = 123
print(message.foo)

需要注意的是,把 foo 的值设定为错误类型,会导致抛出 TypeError。 如果在对 foo 赋值前进行读取,返回的是该字段的默认值。使用 HasField() 来检查字段是否赋值,或用 ClearField() 来清除对字段的赋值。这两个方法都是Message提供的接口。举个例子:

assert not message.HasField("foo")
message.foo = 123
assert message.HasField("foo")
message.ClearField("foo")
assert not message.HasField("foo")

Singular Fields (proto3)

假设你有一个非message类型的单一字段(optional 或 required) foo,如果其类型是 int32, 你可以用如下方法对其进行操作:

message.foo = 123
print(message.foo)

需要注意的是,把 foo 的值设定为错误类型,会导致抛出 TypeError。 如果在对 foo 赋值前进行读取,返回的是该字段的默认值。可以使用Message提供的 ClearField() 方法来将已经设定的值恢复为默认值。例如:

message.foo = 123
message.ClearField("foo")

与proto2不同的是,在proto3中不能对单一非message字段使用 HasField(),如果你尝试这么做,将会抛出异常。

Singular Message Fields

message类型会稍微有点不同。你不能直接对一个嵌套的message字段进行赋值。要对父message中的子message字段进行赋值,需要对子message中的任意字段进行赋值。在proto3中,你可以对父message使用 HasField() 方法来检查子message字段是否设置,但是记住这个方法在proto3中不能用在其他类型上。 举个例子,假设我们有如下的 .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 相同的类型的message 中的每个值。

foo.bar.CopyFrom(baz)

需要注意的是,简单得读取 bar 中的字段,不会设置bar(注:单一非message类型读取时会设置默认值)

foo = Foo()
assert not foo.HasField("bar")
print(foo.bar.i)  # Print i's default value
assert not foo.HasField("bar")

如果你想要对一个未赋值的message设定"has"标志,你可以使用 SetInParent() 方法。

foo = Foo()
assert not foo.HasField("bar")
foo.bar.SetInParent()  # Set Foo.bar to a default Bar message
assert foo.HasField("bar")

Repeated Fields

重复字段(翻译成重复字段感觉差点儿意思)表现像Python中的队列对象。和嵌套message一样,你不能直接对该字段进行赋值,但是你可以对他进行操作。例如,给定如下message定义:

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() 方法。

Repeated Message Fields

重复message表现与重复字段类似,除了不能使用类似于Python对象的 append() 方法。相反的,使用 add() 方法来创建一个新的message对象并添加到list中,并将该message返回给调用者以便进行赋值。同时也提供了 extend() 方法来添加一整个messages列表,但是添加的是列表中的每个message的拷贝。这样做的原因是为了让每个message都有唯一的父message,从而避免循环引用和其他由于可变数据类型有多个拥有者导致的混乱。 例如,给定如下message定义:

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 = 47
foo.bars.extend([new_bar])  # Uses extend() to copy

assert len(foo.bars) == 3
assert foo.bars[0].i == 15
assert foo.bars[1].i == 32
assert foo.bars[2].i == 47
assert foo.bars[2] == new_bar      # The extended message is equal,
assert foo.bars[2] is not new_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

Groups (proto2)

注意:group已经被弃用,不建议在创建新message类型时使用,使用嵌套message来替代

Map Fields

给定如下message定义:

message MyMessage {
	map<int32, int32> mapfield = 1;
}

Python生成码map字段的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]

嵌套message字段一样,message不能直接赋值一个map对象。要为一个值为message的map赋值,引用一个未定义的key时,就会产生并返回一个新的子message:

m.message_map[key].submessage_field = 10

你可以在下一节中了解更多未定义key的信息。

引用未定义的key

对于map中未定义的key,在Protocol Buffer中的语法和在python中有轻微的不同。在常规的Python dict中,引用未定义的key会抛出KeyError异常:

>>> x = {}
>>> x[5]
Traceback (most recent call last):
	File "<stdin>", line 1, in <module>
KeyError: 5

但是,在Protocol Buffers的map中,引用一个未定义的key,会为该key在字典中分配一个 zero/false/empty 值。这个表现更接近于Python标准库中的 defaultdict

>>> dict(m.mapfield)
{}
>>> m.mapfield[5]
0
>>> dict(m.mapfield)
{5: 0}

这个特性对包含值为message的map来说非常方便,因为这意味着你可以直接对返回的message中的字段进行更新。

>>> m.message_map[5].foo = 3

需要注意的是即使你不为message中字段进行赋值,map中的子message依然会被创建

>>> m.message_map[10]
<test_pb2.M2 object at 0x7fb022af28c0>
>>> dict(m.message_map)
{10: <test_pb2.M2 object at 0x7fb022af28c0>}

这和普通的嵌套message字段有所不同,普通嵌套message只有在你为任意字段赋值时才会被创建。

当别人单独阅读你的代码 m.message_map[10] 时,可能不能立刻非常清楚地明白你的意图可能是创建子message。因此,我们提供了 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)