Python字节数组的用途是什么?

55 投票
4 回答
38337 浏览
提问于 2025-04-17 12:07

我最近在Python中遇到了一个叫做bytearray的数据类型。有人能举一些需要用到bytearray的场景吗?

4 个回答

5

如果你查看bytearray的文档,它会告诉你:

返回一个新的字节数组。bytearray类型是一个可变的整数序列,范围在0到255之间。

bytes的文档则说:

返回一个新的“字节”对象,它是一个不可变的整数序列,范围同样在0到255之间。bytesbytearray的不可变版本——它有相同的不改变内容的方法,以及相同的索引和切片行为。

从中可以看出,主要的区别在于可变性。str的方法如果“改变”了字符串,实际上是返回一个新的字符串,包含了你想要的修改。而bytearray的方法如果改变了序列,则是真正改变了这个序列

如果你需要通过二进制表示来编辑一个大的对象(比如图像的像素缓冲区),并且希望修改能直接在原地进行以提高效率,那么你会更倾向于使用bytearray

59

这个回答是从这里抄来的,真是没羞没臊。

例子 1:从碎片组装消息

假设你正在写一些网络代码,通过套接字连接接收一条大消息。如果你了解套接字,你就知道recv()这个操作不会等所有数据都到齐。它只是返回系统缓冲区中当前可用的数据。因此,为了获取所有数据,你可能会写出这样的代码:

# remaining = number of bytes being received (determined already)
msg = b""
while remaining > 0:
    chunk = s.recv(remaining)    # Get available data
    msg += chunk                 # Add it to the message
    remaining -= len(chunk)  

这个代码唯一的问题是,使用拼接(+=)的性能非常差。因此,在Python 2中,一个常见的性能优化方法是把所有的片段收集到一个列表中,最后再进行拼接。像这样:

# remaining = number of bytes being received (determined already)
msgparts = []
while remaining > 0:
    chunk = s.recv(remaining)    # Get available data
    msgparts.append(chunk)       # Add it to list of chunks
    remaining -= len(chunk)  
msg = b"".join(msgparts)          # Make the final message

现在,这里有第三种解决方案,使用bytearray

# remaining = number of bytes being received (determined already)
msg = bytearray()
while remaining > 0:
    chunk = s.recv(remaining)    # Get available data
    msg.extend(chunk)            # Add to message
    remaining -= len(chunk)  

注意bytearray版本的代码非常简洁。你不需要把部分数据收集到列表中,也不需要在最后进行那种复杂的拼接。真不错。

当然,最大的疑问是它的性能如何。为了测试这一点,我首先创建了一个小的字节片段列表,像这样:

chunks = [b"x"*16]*512

然后我使用timeit模块来比较以下两个代码片段:

# Version 1
msgparts = []
for chunk in chunks:
    msgparts.append(chunk)
msg = b"".join(msgparts)
#Version 2
msg = bytearray()
for chunk in chunks:
    msg.extend(chunk)

经过测试,版本1的代码运行了99.8秒,而版本2运行了116.6秒(使用+=拼接的版本则需要230.3秒)。所以虽然进行拼接操作仍然更快,但也只是快了大约16%。就我个人而言,我觉得bytearray版本的简洁编程可能弥补了这个差距。

例子 2:二进制记录打包

这个例子是对上一个例子的一个小变形。假设你有一个大的Python列表,里面是整数(x,y)坐标。像这样: points = [(1,2),(3,4),(9,10),(23,14),(50,90),...] 现在,假设你需要将这些数据写入一个二进制编码的文件,文件格式是先写一个32位整数表示长度,然后每个点打包成一对32位整数。可以用struct模块来实现,像这样:

import struct
f = open("points.bin","wb")
f.write(struct.pack("I",len(points)))
for x,y in points:
    f.write(struct.pack("II",x,y))
f.close()

这个代码唯一的问题是,它执行了大量的小write()操作。另一种方法是把所有数据打包到一个bytearray中,最后只执行一次写入。例如:

import struct
f = open("points.bin","wb")
msg = bytearray()
msg.extend(struct.pack("I",len(points))
for x,y in points:
    msg.extend(struct.pack("II",x,y))
f.write(msg)
f.close()

果然,使用bytearray的版本运行得快得多。在一个简单的计时测试中,涉及100000个点的列表,它的运行时间大约是进行大量小写入的版本的一半。

例子 3:字节值的数学处理

由于bytearray表现得像整数数组,这使得进行某些类型的计算变得更容易。在最近的一个嵌入式系统项目中,我使用Python通过串口与设备通信。作为通信协议的一部分,所有消息都必须用一个纵向冗余检查(LRC)字节进行签名。LRC是通过对所有字节值进行异或运算来计算的。bytearray使得这样的计算变得简单。这里有一个版本:

message = bytearray(...)     # Message already created
lrc = 0
for b in message:
    lrc ^= b
message.append(lrc)          # Add to the end of the message

这是一个可以增加你工作安全感的版本: message.append(functools.reduce(lambda x,y:x^y,message)) 而这是在没有bytearray的Python 2中的相同计算:

message = "..."       # Message already created
lrc = 0
for b in message:
    lrc ^= ord(b)
message += chr(lrc)        # Add the LRC byte

就我个人而言,我喜欢bytearray版本。没有必要使用ord(),你可以直接把结果添加到消息的末尾,而不是使用拼接。

这里还有一个有趣的例子。假设你想对一个bytearray进行简单的异或加密。这里有一个一行代码可以做到:

>>> key = 37
>>> message = bytearray(b"Hello World")
>>> s = bytearray(x ^ key for x in message)
>>> s
bytearray(b'm@IIJ\x05rJWIA')
>>> bytearray(x ^ key for x in s)
bytearray(b"Hello World")
>>> 

这里是演示的链接。

59

bytearray 和普通的 Python 字符串(在 Python 2.x 中是 str,在 Python 3 中是 bytes)很相似,但有一个重要的区别:字符串是不可变的,而 bytearray 是可变的,像是一个单个字符字符串的list

这很有用,因为有些应用需要处理字节序列,而用不可变字符串处理起来效率不高。当你在大块内存中频繁进行小改动时,比如在数据库引擎或图像库中,字符串的表现就不太好;因为你需要复制整个(可能很大的)字符串。而 bytearray 的好处在于,它可以让你在不先复制内存的情况下进行这种修改。

不过,这种情况其实是例外,而不是常态。大多数情况下,我们是用来比较字符串或者格式化字符串。对于格式化字符串,通常还是会有一个复制,所以可变类型并没有优势。而对于比较字符串,由于不可变字符串不能改变,你可以先计算字符串的hash值,然后用这个值来比较,这样比逐个比较每个字节要快得多;所以不可变类型(strbytes)是默认的,而 bytearray 则是当你需要它特殊功能时的例外。

撰写回答