Python中字节字面量的比较
这个问题是因为我想用 bytes
字符串作为字典的键,而我认为相等的字节值却没有被当作相等来处理。
为什么下面这段 Python 代码比较后不相等呢?难道这两个不是同样的二进制数据表示吗?(这个例子特意选择了避免字节序的问题)
b'0b11111111' == b'0xff'
我知道下面这个是成立的,证明了它们是等价的:
int(b'0b11111111', 2) == int(b'0xff', 16)
但是为什么 Python 要我了解它们的表示方式呢?这和字节序有关吗?有没有简单的方法可以让它们比较时被认为是相等的,而不是把它们都转换成,比如说,十六进制字面量?有没有一种透明且清晰的方法可以在所有表示之间转换,且在某种程度上不依赖于平台(或者我是不是要求太多了)?
假设我想用 8 位的形式 b'0b11111111'
来索引一个字典,那为什么 Python 会把它扩展到十个字节,我该如何防止这种情况发生?
这是一个大型树形数据结构中的一小部分,把我的索引扩展到 80 倍似乎是巨大的内存浪费。
3 个回答
看起来你想要得到一个表示值 0b11111111
(也就是255)的字节串。但 b'0b11111111'
并不是这个意思——它实际上是表示字符(Unicode)字符串 '0b11111111'
的字节串。
你想要的应该写成 b'\xff'
。你可以检查一下,它确实是一个字节:len(b'\xff') == 1
。
如果你想把一个Python的 int
转换成二进制表示,可以使用 ctypes
这个库。你需要选择一种C语言的整数类型,比如:
>>> bytes(ctypes.c_ubyte(255))
b'\xff'
>>> bytes(ctypes.c_ubyte(0xff))
b'\xff'
>>> bytes(ctypes.c_long(255))
b'\xff\x00\x00\x00\x00\x00\x00\x00'
注意:你可以用别名 c_uint8
(即8位无符号C整数)和 c_int64
(64位有符号C整数)来代替 c_ubyte
和 c_long
。
如果要转换回去:
>>> ctypes.c_ubyte.from_buffer_copy(b'\xff').value
255
要小心溢出的问题:
>>> ctypes.c_ubyte(256)
c_ubyte(0)
b'0b11111111'
这个东西由 10 个字节组成:
In [44]: list(b'0b11111111')
Out[44]: ['0', 'b', '1', '1', '1', '1', '1', '1', '1', '1']
而 b'0xff'
这个东西只有 4 个字节:
In [45]: list(b'0xff')
Out[45]: ['0', 'x', 'f', 'f']
很明显,它们不是同一个东西。
Python 非常重视明确性。(明确比模糊好。)它不会 假设 b'0b11111111'
一定是一个整数的二进制表示。它只是一个字节字符串。你怎么理解它,必须明确说明。
字节可以表示任何东西。Python 不会也无法猜测你的字节可能编码的内容。
举个例子,int(b'0b11111111', 34)
也是一种有效的解释,但这种解释和十六进制的FF是不一样的。
实际上,解释的方式是无穷无尽的。这些字节可以表示一系列的ASCII码,或者图像颜色,或者音乐音符。
在你明确给出解释之前,字节对象仅仅是由0到255范围内的值组成,而这些字节的文本表示如果可以被打印出来,就会使用ASCII:
>>> list(bytes(b'0b11111111'))
[48, 98, 49, 49, 49, 49, 49, 49, 49, 49]
>>> list(bytes(b'0xff'))
[48, 120, 102, 102]
这些字节序列是不相等的。
如果你想把这些序列明确地解释为整数常量,可以使用ast.literal_eval()
来解释解码后的文本值;在比较之前,始终要先进行标准化:
>>> import ast
>>> ast.literal_eval(b'0b11111111'.decode('utf8'))
255
>>> ast.literal_eval(b'0xff'.decode('utf8'))
255