在不修改Python源代码的情况下,有什么方法可以向struct模块的格式规范小语言添加新的格式字符?
我正在使用Modbus协议从机器读取数据。如果你对Modbus不太了解,我真心羡慕你。说正经的,Modbus是一种底层协议,它允许你通过指定起始地址和要读取的寄存器数量来读取机器中的“寄存器”。“寄存器”其实就是两个字节的数据。实现Modbus的机器通常只懂得如何在寄存器中编码整数(比如int、short等)和浮点数。所以,如果你需要表示其他类型的数据,就得想办法把它们转换成一系列整数和浮点数,然后再想办法把这些整数和浮点数组合成你需要的实际数据类型。
这本来没什么问题,但我正在通信的机器使用了一种奇怪的组合,既有小端格式又有大端格式。具体来说,整数、短整数等都是用大端格式编码的,但浮点数的编码格式既不是大端也不是小端。例如,如果我们有一个浮点值x,在大端格式下表示为01 02 03 04
,在小端格式下表示为04 03 02 01
,那么这些机器会把x表示为03 04 01 02
。我不知道这个格式有什么名字,所以我就叫它“寄存器交换的大端格式”,因为如果你只是交换前两个寄存器的内容,它就会变成大端格式。
到现在为止,我一直是通过在读取浮点数时交换字节1和2与字节3和4的位置来解码寄存器交换的浮点数,然后再把它传给struct.unpack(">f")
。这样做一直没问题,因为我读取的数据只是整数和浮点数。
然而,我写的数据收集软件的要求最近扩大了。现在不能再简单地假设浮点数就是浮点数,整数就是整数,因为我需要支持更复杂的数据类型,这些类型由多个整数和/或浮点数组成。例如,一系列六个无符号短整数和一个浮点数,它们一起表示一个带偏移的时间戳。如果这些浮点数是大端格式,就像机器记录的其他值一样,我只需将数据传给struct.unpack(">HHHHHHf")
,然后把输出传给datetime.datetime()
,但它们并不是以大端格式编码的——在实际解码之前,我仍然需要交换任何我读取的浮点数的字节1和2与字节3和4的位置。但由于我现在读取的数据由多个单独的值组成,我不能再盲目地交换字节1和2与字节3和4的位置——我需要确切知道浮点数在我读取的字节中位于哪里。
我想到的最简单的处理方法是给struct
模块的格式说明小语言添加一个自定义格式代码,来表示“寄存器交换的浮点数”,也许用字符"r"
。这样,我就可以直接使用struct.unpack(">HHHHHHr")
或struct.unpack(">rr")
,或者其他我需要的格式,轻松搞定。然而,似乎struct
模块的格式说明小语言直接来自于Python的底层C实现。
所以,我想我可以创建一个struct.Struct
的子类,检查传入的格式字符串中是否有"r"
,在必要时对"r"
类型的浮点数进行字节交换,构建一个新的格式字符串,把所有的"r"
替换成"f"
,然后用这个新格式字符串来unpack()
数据。但为了能够正确处理任何任意的格式字符串,我实际上需要重新实现所有的结构格式说明小语言的解析规则,这样做可真是个糟糕的主意。
那么,有没有办法让我在Python中添加自定义格式代码,或者实现类似的功能,而不需要修改底层的C代码呢?
1 个回答
我最开始问的问题的技术性答案是:“不,没办法在结构模块的格式小语言中添加新的格式代码,除非修改Python底层的C实现。”
不过,我实际想解决的问题的实用性答案要积极得多,虽然比我最开始希望的要复杂一些。
先给你点背景,我已经有了一些对象来处理注册字节的过程,把它们解码成底层的基本值,或者把这些基本值转换成实际的目标数据类型。具体来说,我定义了一个Decoder
类,用来把注册字节解码成底层的整数、浮点数等的元组,还有一个Caster
类,用来把一个或多个整数、浮点数的元组转换成时间戳或其他数据类型。
我最开始定义了一小组预定义的Decoder
对象,每个对象都接受一个预定义的struct.Struct
作为初始化参数。Decoder.decode(register_bytes)
方法实际上就是把注册字节传给底层的结构体去.unpack()
。我还有一个Decoder
的子类叫RegswapDecoder
,它只是简单地把字节1和2与字节3和4交换,以处理奇怪的浮点格式。当我读取的所有寄存器只表示单个值时,这一切都很好,但这显然不够灵活,无法处理我在问题中提到的新复合数据类型的复杂格式。
直接使用Struct
不够灵活,这种不灵活性让我很头疼,所以我重构了Decoder
类,让它接受一个任意定义的“解码函数”作为初始化参数,而不是Struct
。然后,我用几个预定义的函数替换了之前创建的预定义Struct
。从技术上讲,这些解码函数大多数只是调用一个预定义Struct
的.unpack()
方法,因为这就是它们实际需要的,但使用函数而不是仅仅使用Struct
的灵活性意味着我可以处理包含那些麻烦浮点数的复合数据类型。我确实需要为每种复合类型定义一个新的转换函数,但除了调用某个Struct
的.unpack()
方法之外,我在这些函数中需要编写的代码只是交换表示麻烦浮点数的字节位置,所以写起来并不难。如果有很多复合类型使用这些麻烦的浮点数,我会有一大堆重复的代码,只是交换这些浮点数的字节1+2和3+4,这有点让人失望,但实际上,由于Modbus协议的底层限制,我需要支持的复合类型不会太多。而且作为额外的好处,如果我们以后开始使用另一种品牌的PLC,它以另一种方式处理数据……我已经有了处理它所需的灵活性。
考虑到我本来就得为每种数据类型创建一个新的Struct
,所以为每种类型定义一个解码函数其实并不算什么。而且在我新的方法中,我还能够去掉RegswapDecoder
类——越想越觉得那个类的设计其实并不好。
总的来说,虽然我仍然更希望能够简单地扩展struct
模块的格式小语言,但我觉得这个解决方案足够好,能够实现我实际需要完成的任务。