找回密码
 立即注册

QQ登录

只需一步,快速开始

在Python中使用Protocol Buffers的具体先容

2024-11-5 00:54| 发布者: 2ae29| 查看: 201| 评论: 0

摘要: 目录实践环境问题域界说协议格式(编写proto文件)编译协议缓冲区协议缓冲区 API枚举标准消息方法分析和序列化编写消息读取消息另一个示例参考链接实践环境 protoc-25.4-win64.zip 下载地址: https://github.com/prot
目录

实践环境

protoc-25.4-win64.zip

下载地址:

https://github.com/protocolbuffers/protobuf/releases

https://github.com/protocolbuffers/protobuf/releases/download/v25.4/protoc-25.4-win64.zip

protobuf 5.27.2

[code]pip install protobuf==5.27.2[/code]

Python 3.9.13

问题域

本文将使用的示例是一个非常简单的“地址簿”应用步伐,它可以从文件中读取和写入人们的联系方式。通讯簿中的每个人都有一个姓名、一个ID、一个电子邮件地址和一个联系电话号码。

如何序列化和检索如许的结构化数据?有几种方法可以办理这个问题:

使用Python pickle。这是默认方法,由于它内置于语言中,但它不能很好地处置惩罚模式演化,如果你必要与用C++或Java编写的应用步伐共享数据,它也不能很好的工作。

你可以发明一种特殊的方法将数据项编码为单个字符串,比方将4个整数编码为“12:3:-23:67”。这是一种简单而机动的方法,只管它确实必要编写一次性编码和分析代码,而且分析带来的运行时成本很小。这最适合对非常简单的数据进行编码。

将数据序列化为XML。这种方法非常有吸引力,由于XML(某种程度上)是人类可读的,而且有许多语言的绑定库。如果想与其他应用步伐/项目共享数据,这大概是一个不错的选择。然而,众所周知,XML是空间麋集型的,对其进行编码/解码会给应用步伐带来巨大的性能损失。此外访问XML DOM树访问类中的简单字段要复杂得多。

可以使用协议缓冲区(Protocol buffers)替换这些选择。协议缓冲区是办理这个问题的机动、高效、主动化的办理方案。使用协议缓冲区 ,可以编写渴望存储的数据结构的[code].proto[/code]形貌。协议缓冲区编译器将从该文件创建一个类,该类以有效的二进制格式实现协议缓冲区数据的主动编码和分析。天生的类为构成协议缓冲区的字段提供[code]getters[/code]和[code]setters[/code]方法,并处置惩罚将协议缓冲区作为一个单元进行读写的细节。重要的是,协议缓冲区格式支持随着时间的推移扩展格式的想法,如许代码仍旧可以读取用旧格式编码的数据。

界说协议格式(编写proto文件)

要创建地址簿应用步伐,必要从[code].proto[/code]文件开始。[code].proto[/code]文件中的界说很简单:为要序列化的每个数据结构添加一个消息([code]message[/code]),然后为消息中的每个字段指定名称和范例。

示例:[code]addressbook.proto[/code]

[code]syntax = "proto2"; // proto2指定proto buffer的版本 package tutorial; message Person { optional string name = 1; optional int32 id = 2; optional string email = 3; enum PhoneType { PHONE_TYPE_UNSPECIFIED = 0; PHONE_TYPE_MOBILE = 1; PHONE_TYPE_HOME = 2; PHONE_TYPE_WORK = 3; } message PhoneNumber { optional string number = 1; optional PhoneType type = 2 [default = PHONE_TYPE_HOME]; } repeated PhoneNumber phones = 4; // phones字段是一个重复字段,可以包含多个电话号码。 } message AddressBook { repeated Person people = 1; }[/code]

阐明:

以上这个[code].proto[/code]文件以[code]package[/code]声明开始,这有助于防止不同项目之间的定名冲突。在Python中,包通常由目录结构决定,因此在[code].proto[/code]文件界说的[code]package[/code]对天生的代码没有影响。但是,仍旧应该声明一个[code]package[/code],以避免在协议缓冲区名称空间以及非Python语言中的名称冲突。

接下来,是消息界说。消息只是包含一组范例字段的集合。许多标准的简单数据范例可以作为字段范例使用,包括[code]bool[/code]、[code]int32[/code]、[code]float[/code]、[code]double[/code]和[code]string[/code]。还可以通过使用其他消息范例作为字段范例来为消息添加更多的结构 - 在上面的示例中,[code]Person[/code]消息包含[code]PhoneNumber[/code]消息,而[code]AddressBook[/code]消息包含[code]Person[/code]消息。甚至可以界说嵌套在其他消息中的消息范例-如上,[code]PhoneNumber[/code]范例界说在[code]Person[/code]中。如果渴望其中一个字段具有预界说的值列表之一,也可以界说枚举范例 - 在这里渴望指定电话号码可以是以下电话范例之一:

  • [code]PHONE_TYPE_MOBILE[/code]
  • [code]PHONE_TYPE_HOME[/code]
  • [code]PHONE_TYPE_WORK[/code]

每个元素上的“=1”、“=2”标志标识该字段在二进制编码中使用的唯一“标志”,这确保了在序列化和反序列化过程中,‌每个字段可以被精确地辨认和处置惩罚。‌这些数字标签在编译时被转换为定名空间和范例署名,‌从而保证了字段的唯一性。使用1-15的标志编号比使用更高的数字要少一个字节编码,因此作为优化,可以决定将这些标签用于常用或重复的元素,将16及更高标志编号的用于不太常用的可选元素。重复字段中的每个元素都必要重新编码标暗号,因此重复字段特别适合此优化。

每个字段都必须使用以下修饰符之一进行注解:

  • [code]optional[/code]:该字段可以设置,也可以不设置。如果未设置可选字段值,则使用默认值。对于简单范例,可以指定自己的默认值,就像示例中为电话号码[code]type[/code]所做的那样。否则,将使用系统默认值:数字范例的默认值为零,字符串范例的默认值为空字符串,布尔范例的默认值为[code]false[/code]。对于嵌入式消息,默认值始终是消息的“默认实例”或“原型”,其没有设置任何字段。调用访问器以获取尚未显式设置的可选(或必须)字段的值时,始终返回该字段的默认值。
  • [code]repeated[/code]:该字段可以重复任意多次(包括零次),表示该字段可以包含多个值。将重复字段视为动态巨细的数组,重复值的序次将在协议缓冲区中保留。
  • [code]required[/code]:必须提供该字段的值,否则该消息将被视为“未初始化”。序列化未初始化的消息将引发非常。分析未初始化的消息将失败。除此之外,必须字段的行为与可选字段完全相同。
[code]重要[/code]

[code]required[/code]是永久的,在将字段标志为[code]required[/code]时应非常警惕。如果在某个时间渴望克制编写或发送必须字段,将该字段更改为可选字段将很成问题 - 旧的读取器会认为没有此字段的消息不完备,并大概会不测地拒绝或删除它们。你应该思量为协议缓冲区编写特定于应用步伐的自界说验证例程。在Google 剧烈不赞成使用[code]required[/code]字段;在 proto2 语法中界说的大多数消息仅使用[code]optional[/code]和[code]repeated[/code]。(Proto3 根本不支持[code]required[/code]字段。)

编译协议缓冲区

如今有了[code].proto[/code],接下来必要做的就是天生读写 [code]AddressBook[/code](以及 [code]Person[/code] 和 [code]PhoneNumber[/code])消息所需的类。为此,必要在 [code].proto[/code] 上运行协议缓冲区编译器 [code]protoc[/code]:

1、下载[code]protoc[/code]后解压,将[code]protoc[/code]所在[code]bin[/code]目录路径添加到系统环境变量

[code]>protoc --version libprotoc 25.4[/code]

2、如今运行编译器,指定源目录(应用步伐源代码所在的位置 - 如果未提供值,则使用当前目录)、目的目录(渴望天生的代码的存储目录;通常与 [code]$SRC_DIR[/code] 相同)和 [code].proto[/code] 的路径。如下:

[code]protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto[/code]

由于想要 Python 类,所以使用 [code]--python_out[/code] 选项 - 为其他受支持的语言提供了类似的选项。

[code]protoc[/code]还可以使用[code]--pyi_out[/code]天生python存根(.pyi)。

这会在你指定的目的目录中天生对应的[code]xxxx_pb2.py[/code]

实践:cmd打开控制台,进入到[code]addressbook.proto3[/code]所在目录,然后执行以下命令

[code]protoc --python_out=. addressbook.proto2[/code]

命令执行乐成后,会再当前目录下天生与[code].proto2[/code]文件同款式录(例中为[code]addressbook[/code]),目录下主动天生对应的py文件(例中为[code]proto2_pb2.py[/code],实践时将其拷贝到[code]addressbook.proto2[/code]所在目录并从定名为[code]addressbook_pb2.py[/code])

协议缓冲区 API

与天生 Java 和 C++ 协议缓冲区代码不同,Python 协议缓冲区编译器不会直接为你天生数据访问代码。相反(如果你检察 [code]addressbook_pb2.py[/code],你就会看到),它会为你的所有消息、枚举和字段天生特殊形貌符,以及一些秘密的空类,每个消息范例一个类。

[code]# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: addressbook.proto2 # Protobuf Python Version: 4.25.4 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12\x61\x64\x64ressbook.proto2\x12\x08tutorial\"\xa3\x02\n\x06Person\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\x05\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12,\n\x06phones\x18\x04 \x03(\x0b\x32\x1c.tutorial.Person.PhoneNumber\x1aX\n\x0bPhoneNumber\x12\x0e\n\x06number\x18\x01 \x01(\t\x12\x39\n\x04type\x18\x02 \x01(\x0e\x32\x1a.tutorial.Person.PhoneType:\x0fPHONE_TYPE_HOME\"h\n\tPhoneType\x12\x1a\n\x16PHONE_TYPE_UNSPECIFIED\x10\x00\x12\x15\n\x11PHONE_TYPE_MOBILE\x10\x01\x12\x13\n\x0fPHONE_TYPE_HOME\x10\x02\x12\x13\n\x0fPHONE_TYPE_WORK\x10\x03\"/\n\x0b\x41\x64\x64ressBook\x12 \n\x06people\x18\x01 \x03(\x0b\x32\x10.tutorial.Person') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'addressbook.proto2_pb2', _globals) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None _globals['_PERSON']._serialized_start=33 _globals['_PERSON']._serialized_end=324 _globals['_PERSON_PHONENUMBER']._serialized_start=130 _globals['_PERSON_PHONENUMBER']._serialized_end=218 _globals['_PERSON_PHONETYPE']._serialized_start=220 _globals['_PERSON_PHONETYPE']._serialized_end=324 _globals['_ADDRESSBOOK']._serialized_start=326 _globals['_ADDRESSBOOK']._serialized_end=373 # @@protoc_insertion_point(module_scope)[/code]

每个类中的重要行是 [code]__metaclass__ = reflection.GeneratedProtocolMessageType[/code]。可以将它们视为创建类的模板。在加载时,[code]GeneratedProtocolMessageType[/code] 元类使用指定的形貌符来创建使用每种消息范例所需的所有 Python 方法,并将它们添加到相关的类中。然后可以在代码中使用完全添补的类。

所有这一切的最终结果是,你可以使用 [code]Person[/code] 类,就似乎它将 Message 基类的每个字段界说为通例字段一样。比方:

[code]import addressbook_pb2 person = addressbook_pb2.Person() person.id = 1234 person.name = "John Doe" person.email = "jdoe@example.com" phone = person.phones.add() phone.number = "555-4321" phone.type = addressbook_pb2.Person.PHONE_TYPE_HOME[/code]

注意,这些赋值不光仅是向通用 Python 对象添加任意新字段。如果你实验分配 .proto 文件中未界说的字段,则会引发 [code]AttributeError[/code]。如果你将字段分配给错误范例的值,则会引发 [code]TypeError[/code]。此外,在设置字段之前读取字段的值会返回默认值。

枚举

元类将枚举扩展为一组具有整数值的符号常量。因此,比方,常量 [code]addressbook_pb2.Person.PhoneType.PHONE_TYPE_WORK[/code] 的值为 2。

标准消息方法

每个消息类还包含许多其他方法,让你可以检查或操作整个消息,包括:

  • [code]IsInitialized()[/code]: 检查是否已设置所有必须的字段。
  • [code]__str__()[/code]:返回消息的可读表示情势,特别实用于调试。(通常如许调用 [code]str(message)[/code]或 [code]print(message)[/code])
  • [code]CopyFrom(other_msg)[/code]:使用给定消息的值覆盖消息。
  • [code]Clear()[/code]:清除所有元素,使其返回到空状态。

这些方法实现了 Message 接口。有关更多信息,请参阅 Message 的完备 API 文档

分析和序列化

每个协议缓冲区类都具有使用协议缓冲区二进制格式来写入和读取所选范例消息的方法。这些方法包括:

  • [code]SerializeToString()[/code]:序列化消息并将其作为字符串返回。注意,bytes是二进制的,不是文本;仅将 [code]str[/code] 范例用作方便的容器。
  • [code]ParseFromString(data)[/code]:从给定的字符串分析消息。

这些只是用于分析和序列化的选择中的一部分。同样,请参阅Message API 参考以获取完备列表。

[code]重要 协议缓冲区和面向对象设计 协议缓冲区类根本上是数据持有者(如 C 中的结构),不提供其他功能;它们在对象模型中不是好的主要公民。如果想为天生的类添加更丰富的行为,最好的方法是将天生的协议缓冲区类包装在特定于应用步伐的类中。如果你无法控制 [code].proto[/code]文件的设计(比方,如果正在复用来自另一个项目的文件),那么包装协议缓冲区也是一个好主意。在这种情况下,您可以使用包装器类来构建更适合你应用步伐的独特环境的接口:隐藏一些数据和方法,公开便捷功能等。绝不应通过继续天生的类继续来向它们添加行为。这会粉碎内部机制,而且无论如何也不是好的面向对象实践。[/code]

编写消息

假设渴望通讯录应用步伐能够做到的第一件事就是将个人具体信息写入通讯录文件。为此,必要创建和添补协议缓冲区类的实例,然后将它们写入输出流。

这里示例代码从文件中读取 [code]AddressBook[/code],根据用户输入向其中添加一个新 [code]Person[/code],然后将新的 [code]AddressBook[/code] 再次写回文件。直接调用或引用协议编译器天生的代码的部分已突出显示。

[code]#!/usr/bin/env python3 # -*- coding:utf-8 -*- import addressbook_pb2 import os def PromptForAddress(person): '''基于用户输入添补Person消息''' person.id = int(input('Enter person ID number: ')) person.name = input('Enter name: ') email = input('Enter email address (blank for none): ') if email != '': person.email = email while True: number = input('Enter a phone number (or leave blank to finish): ') if number == '': break phone_number = person.phones.add() phone_number.number = number phone_type = input('Is this a mobile, home, or work phone? ') if phone_type == 'mobile': phone_number.type = addressbook_pb2.Person.PhoneType.PHONE_TYPE_MOBILE elif phone_type == 'home': phone_number.type = addressbook_pb2.Person.PhoneType.PHONE_TYPE_HOME elif phone_type == 'work': phone_number.type = addressbook_pb2.Person.PhoneType.PHONE_TYPE_WORK else: print('Unknown phone type; leaving as default value.') address_book = addressbook_pb2.AddressBook() # 读取已存在地址簿 if os.path.exists('my_addressbook.db'): with open('my_addressbook.db', 'rb') as f: address_book.ParseFromString(f.read()) # 添加一个通讯地址 PromptForAddress(address_book.people.add()) # 将通讯地址写到磁盘 with open('my_addressbook.db', 'wb') as f: f.write(address_book.SerializeToString())[/code]

运行步伐后按提示输入内容,形如以下

[code]Enter person ID number: 1 Enter name: shouke Enter email address (blank for none): shouke@163.com Enter a phone number (or leave blank to finish): 15813735565 Is this a mobile, home, or work phone? mobile Enter a phone number (or leave blank to finish): [/code]

读取消息

此示例读取上述示例创建的文件,并打印其中的所有信息

[code]# -*- coding:utf-8 -*- import addressbook_pb2 def ListPeople(address_book): '''遍历地址簿中的所有people并打印相关信息''' for person in address_book.people: print('Person ID: ', person.id) print('Name: ', person.name) if person.HasField('email'): print('E-mail address: ', person.email) for phone_number in person.phones: if phone_number.type == addressbook_pb2.Person.PhoneType.PHONE_TYPE_MOBILE: print('Mobile phone #: ', end='') elif phone_number.type == addressbook_pb2.Person.PhoneType.PHONE_TYPE_HOME: print('Home phone #: ', end='') elif phone_number.type == addressbook_pb2.Person.PhoneType.PHONE_TYPE_WORK: print('Work phone #: ', end='') print(phone_number.number) address_book = addressbook_pb2.AddressBook() # 读取已存在地址簿 with open('my_addressbook.db', 'rb') as f: address_book.ParseFromString(f.read()) ListPeople(address_book)[/code]

运行输出:

[code]Person ID: 1 Name: shouke E-mail address: shouke@163.com Mobile phone #: 15813735565[/code]

另一个示例

例子中,界说了一个名为[code]Device[/code]的消息,它有4个字段:[code]name[/code]、[code]price[/code],[code]type[/code]和[code]labels[/code]。

device.proto

[code]syntax = "proto3"; message Device { string name = 1; int32 price = 2; string type = 3; map<string, string> labels = 15; }[/code]

根据[code]device.proto[/code]文件天生python文件

[code]protoc --python_out=. device.proto[/code]

主动在当前目录下天生[code]device[/code]目录及[code]device/proto3_pb2.py[/code]文件

使用天生的py文件(拷贝上述py文件并重定名为[code]device_pb2.py[/code],和以下文件存放在同级目录)

my_test.py

[code]# -*- coding:utf-8 -*- import device_pb2 # 创建一个Person对象并设置字段值 device = device_pb2.Device() device.name = '联想小星' device.price = 3999 device.type = 'Notebook' device.labels['color'] = 'red' device.labels['outlook'] = 'fashionable' # 序列化Person对象为二进制字符串 serialized_device = device.SerializeToString() print(f"序列化后的数据:{serialized_device}") # 反序列化二进制字符串为一个新的Person对象 new_device = device_pb2.Device() new_device.ParseFromString(serialized_device) # 输出新的Device对象的字段值 print(type(new_device.labels)) # <class 'google._upb._message.ScalarMapContainer'> for label, value in new_device.labels.items(): print(label, value) # 输出内容形如:color red print(new_device.labels) # {'color': 'red', 'outlook': 'fashionable'} print(f'反序列化后的数据:设备名称={new_device.name}, 价格={new_device.price}, 范例={new_device.type}, 标签={new_device.labels}') # 输出:反序列化后的数据:设备名称=联想小星, 价格=3999, 范例=Notebook, 标签={'color': 'red', 'outlook': 'fashionable'}[/code]

参考链接

https://protobuf.dev/getting-started/pythontutorial/

https://protobuf.com.cn/getting-started/pythontutorial/

https://protobuf.dev/programming-guides/proto3/

到此这篇关于在Python中使用Protocol Buffers底子先容的文章就先容到这了,更多相关Python使用Protocol Buffers内容请搜索脚本之家以前的文章或继续欣赏下面的相关文章渴望大家以后多多支持脚本之家!


来源:https://www.jb51.net/python/3287038qm.htm
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

最新评论

关闭

站长推荐上一条 /6 下一条

QQ|手机版|小黑屋|梦想之都-俊月星空 ( 粤ICP备18056059号 )|网站地图

GMT+8, 2025-8-16 02:03 , Processed in 0.045066 second(s), 19 queries .

Powered by Mxzdjyxk! X3.5

© 2001-2025 Discuz! Team.

返回顶部