将可变大小的字节数组转换为整数/长整型

56 投票
2 回答
137773 浏览
提问于 2025-04-18 16:57

我该如何把一个(大端序)可变大小的二进制字节数组转换成一个(无符号的)整数或长整型呢?比如说,'\x11\x34' 这个字节数组,它代表的数字是4404。

现在,我正在使用

def bytes_to_int(bytes):
  return int(bytes.encode('hex'), 16)

这个方法虽然小巧且有点易读,但可能效率不是很高。有没有更好(更明显)的办法呢?

2 个回答

2

函数 struct.unpack(...) 正是你需要的功能。

104

Python 传统上对“以大端 C 布局表示的数字”并没有太多用处,尤其是当这些数字超出 C 的范围时。(如果你在处理 2 字节、4 字节或 8 字节的数字,那么 struct.unpack 就是解决方案。)

不过,有很多人对没有一个明显的方法来处理这个问题感到厌烦,因此在 Python 3.2 中增加了一个方法 int.from_bytes,它正好满足你的需求:

int.from_bytes(b, byteorder='big', signed=False)

可惜的是,如果你使用的是旧版本的 Python,就没有这个方法。那么,你还有哪些选择呢?(除了显而易见的:更新到 3.2,或者更好的是 3.4……)


首先,看看你的代码。我认为 binascii.hexlify.encode('hex') 更好,因为“编码”这个词在字节字符串(与 Unicode 字符串相比)上总让我觉得有点奇怪,实际上在 Python 3 中已经被淘汰了。不过,除此之外,我觉得这个方法还是挺容易理解的。而且它的速度应该也不错——是的,它需要创建一个中间字符串,但所有的循环和计算都是在 C 语言中完成的(至少在 CPython 中),这通常比在 Python 中快一个数量级或两个。除非你的 bytearray 大到分配字符串本身就很耗时,否则我觉得性能问题不必太担心。

另外,你也可以用循环来实现。但这样会显得更冗长,而且在 CPython 中会慢很多。

你可以尝试用隐式循环来替代显式循环,但显而易见的函数是 reduce,不过社区中有一部分人认为这个方法不够 Python 风格——而且当然,它需要为每个字节调用一次函数。

你可以通过将循环展开,或者用 8 字节为一组,使用 struct.unpack_from 来循环,或者直接做一个大的 struct.unpack('Q'*len(b)//8 + 'B' * len(b)%8),然后再循环,但这样会让代码可读性大大降低,而且可能速度提升也不明显。

你还可以使用 NumPy……但如果你要处理的数字超过 64 位或者可能 128 位,它最终还是会把所有东西转换成 Python 对象。

所以,我认为你的方法是最好的选择。


这里有一些时间比较,和最明显的手动转换进行对比:

import binascii
import functools
import numpy as np

def hexint(b):
    return int(binascii.hexlify(b), 16)

def loop1(b):
    def f(x, y): return (x<<8)|y
    return functools.reduce(f, b, 0)

def loop2(b):
    x = 0
    for c in b:
        x <<= 8
        x |= c
    return x

def numpily(b):
    n = np.array(list(b))
    p = 1 << np.arange(len(b)-1, -1, -1, dtype=object)
    return np.sum(n * p)

In [226]: b = bytearray(range(256))

In [227]: %timeit hexint(b)
1000000 loops, best of 3: 1.8 µs per loop

In [228]: %timeit loop1(b)
10000 loops, best of 3: 57.7 µs per loop

In [229]: %timeit loop2(b)
10000 loops, best of 3: 46.4 µs per loop

In [283]: %timeit numpily(b)
10000 loops, best of 3: 88.5 µs per loop

在 Python 3.4 中的比较:

In [17]: %timeit hexint(b)
1000000 loops, best of 3: 1.69 µs per loop

In [17]: %timeit int.from_bytes(b, byteorder='big', signed=False)
1000000 loops, best of 3: 1.42 µs per loop

所以,你的方法依然相当快……

撰写回答