<0x00> 前言
处理二进制文件或者从网络接收字节流时,字节流中的结构化数据可能存在二进制有符号数。虽然开发者根据字节流协议可以先验的知道有符号数的字节序、字长、符号位等信息,但在使用Python进行类型转换时缺少将这些信息显式传递给Python解释器的手段。本文介绍了两种在Python开发中处理二进制有符号数的方法。
<0x01> Python如何处理数字类型
很多编程语言在处理有符号数时将数字的最高位作为符号位,但Python的数字类型实现有所不同。以下是CPython中对长整形的定义:(以下皆为Python 2.7版本)
include/longobject.h
[cpp] view plain copy
/ Long (arbitrary precision) integer object interface /
typedef struct _longobject PyLongObject; / Revealed in longintrepr.h /
include/longintrepr.h
[cpp] view plain copy
/* Long integer representation.
The absolute value of a number is equal to
SUM(for i=0 through abs(ob_size)-1) ob_digit[i] * 2**(SHIFT*i)
Negative numbers are represented with ob_size < 0;
zero is represented by ob_size == 0.
In a normalized number, ob_digit[abs(ob_size)-1](the most significant
digit) is never zero. Also, in all cases, for all valid i,
0 <= ob_digit[i] <= MASK.
The allocation function takes care of allocating extra memory
so that ob_digit[0] ... ob_digit[abs(ob_size)-1] are actually available.
CAUTION: Generic code manipulating subtypes of PyVarObject has to
aware that longs abuse ob_size's sign bit.
*/
struct _longobject {
PyObject_VAR_HEAD
digit ob_digit[1];
};
include/object.h
[cpp] view plain copy
/* PyObject_VAR_HEAD defines the initial segment of all variable-size
PyObject_HEAD \
Py_ssize_t ob_size; /* Number of items in variable part */
从以上代码以及注释中我们知道,Python中的长整形,即_longobject,其符号位是通过PyObject_VAR_HEAD中的ob_size来表示,而ob_digit只存储着长整形的绝对值。
另一方面,当我们初始化一个长整形对象(即将一个数值赋值给对象)的时候,Python解释器并不会把这个数值的最高位当作符号位而是将其当作有效位。
下面以数值-500为例,来看看使用该数值对Python对象赋值会发生什么,简便起见,这里使用16bit字长。
我们知道,为了计算方便,负数在计算机中以其二进制补码形式存储,我们首先将-500转换为二进制补码:
十进制 -500
十六进制 -0x01 F4
二进制 -0b 0000 0001 1111 0100
二进制原码(符号在最高位) 0b 1000 0001 1111 0100
二进制反码(除符号位外,对原码按位取反) 0b 1111 1110 0000 1011
二进制补码(二进制反码加1) 0b 1111 1110 0000 1100
二进制补码的十六进制表示 0xFE 0C
下面代码模拟从字节流中接收到一个大字节序的二进制串(0xFE 0x0C)并把它赋值给一个Python变量然后打印该变量的值:
[python] view plain copy
stream = 'xFEx0C'
number = (ord(stream[0]) << 8) + ord(stream[1])
'0x{:0X}'.format(number)
'0xFE0C'
print number
65036
这显然不是我们想要的结果,为了让Python在以有符号数对对象进行初始化时正确工作,上面的代码需要修改。
<0x02> 使用负号传递数值的符号信息
现在我们知道Python并不从数字的符号位读取符号信息,为了让Python正确的处理该数值,开发者必须显示的通知Python解释器:这是一个负数。开发者可以借助负号(-),即negative操作符来完成这个任务。
先看一下negtive操作符在CPython中的实现:
Objects/longobject.c
[cpp] view plain copy
static PyObject *
long_neg(PyLongObject *v)
{
PyLongObject *z;
if (v->ob_size == 0 && PyLong_CheckExact(v)) {
/* -0 == 0 */
Py_INCREF(v);
return (PyObject *) v;
}
z = (PyLongObject *)_PyLong_Copy(v);
if (z != NULL)
z->ob_size = -(v->ob_size);
return (PyObject *)z;
}
可见negtive操作符只对_longobject的ob_size域(即Python记载的符号位)进行了negtive操作,而ob_digit域不变。
我们还知道,如果数值value为负数,则有value = - abs(value)。当我们将负数的绝对值和negtive操作符一起传递给Python解释器,Python解释器就能正确的处理该数值。
对于本例中的二进制串0xFE 0x0C,我们已经先验的知道了该串的字节序,字长信息,所以其符号位在最高位即bit15。因符号位为1,则该数值为负数。下一步是对该数值求绝对值,由于数值是二进制补码形式存储,则其绝对值为二进制补码减去1(即其二进制反码)再按位取反。
代码如下:
[python] view plain copy
number = 0xFE0C
if (number & 0x8000) != 0:
... number = -((number - 1) ^ 0xFFFF)
...
print number
-500
细心的读者可能会注意到上面代码中按位取反操作并没有使用~(即invert操作符),而是使用的和0xFFFF异或来实现。
如果改为invert操作符会如何呢?
[python] view plain copy
number = 0xFE0C
if (number & 0x8000) != 0:
... number = -(~(number - 1))
...
print number
65036
还是让我们来看一下CPython对invert操作符的实现:
Objects/longobject.c
[cpp] view plain copy
static PyObject *
long_invert(PyLongObject *v)
{
/* Implement ~x as -(x+1) */
PyLongObject *x;
PyLongObject *w;
w = (PyLongObject *)PyLong_FromLong(1L);
if (w == NULL)
return NULL;
x = (PyLongObject *) long_add(v, w);
Py_DECREF(w);
if (x == NULL)
return NULL;
Py_SIZE(x) = -(Py_SIZE(x));
return (PyObject *)x;
}
正如代码中的注释说明,Python的invert操作符的实现并不是真的按位取反,而是对数值加1后的negtive操作。
<0x03> 一个更通用的库
struct是一个对二进制结构化数据解包与打包的库,可以让你使用对开发者更友好的方式来传递字节序、字长、符号位等信息,同时也有着足够的错误报告机制,让你的开发更高效。
[python] view plain copy
import struct
stream = 'xFEx0C'
number, = struct.unpack('>h', stream)
print number
-500
'>h'是struct支持的格式化串,‘>'表明字节流为大字节序,‘h'表明字节流包含signed short。关于struct模块更多的说明可参见这个链接。
版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。