Python字节数组的用途是什么?
我最近在Python中遇到了一个叫做bytearray
的数据类型。有人能举一些需要用到bytearray的场景吗?
4 个回答
如果你查看bytearray
的文档,它会告诉你:
返回一个新的字节数组。
bytearray
类型是一个可变的整数序列,范围在0到255之间。
而bytes
的文档则说:
返回一个新的“字节”对象,它是一个不可变的整数序列,范围同样在0到255之间。
bytes
是bytearray
的不可变版本——它有相同的不改变内容的方法,以及相同的索引和切片行为。
从中可以看出,主要的区别在于可变性。str
的方法如果“改变”了字符串,实际上是返回一个新的字符串,包含了你想要的修改。而bytearray
的方法如果改变了序列,则是真正改变了这个序列。
如果你需要通过二进制表示来编辑一个大的对象(比如图像的像素缓冲区),并且希望修改能直接在原地进行以提高效率,那么你会更倾向于使用bytearray
。
这个回答是从这里抄来的,真是没羞没臊。
例子 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")
>>>
这里是演示的链接。
bytearray
和普通的 Python 字符串(在 Python 2.x 中是 str
,在 Python 3 中是 bytes
)很相似,但有一个重要的区别:字符串是不可变的,而 bytearray
是可变的,像是一个单个字符字符串的list
。
这很有用,因为有些应用需要处理字节序列,而用不可变字符串处理起来效率不高。当你在大块内存中频繁进行小改动时,比如在数据库引擎或图像库中,字符串的表现就不太好;因为你需要复制整个(可能很大的)字符串。而 bytearray
的好处在于,它可以让你在不先复制内存的情况下进行这种修改。
不过,这种情况其实是例外,而不是常态。大多数情况下,我们是用来比较字符串或者格式化字符串。对于格式化字符串,通常还是会有一个复制,所以可变类型并没有优势。而对于比较字符串,由于不可变字符串不能改变,你可以先计算字符串的hash
值,然后用这个值来比较,这样比逐个比较每个字节要快得多;所以不可变类型(str
或 bytes
)是默认的,而 bytearray
则是当你需要它特殊功能时的例外。