Python基础指南之字节类型bytes与字节数组bytearray区别详解
一、开篇:字符串之下,还有更底层的东西
前面几篇文章我们都在讲字符串(str)的各种操作。str是Python中处理文本的绝对主力——但当你处理网络数据包、读取图片文件、解析二进制协议、做加密解密时,你需要的不是str,而是bytes。
bytes(字节串)和bytearray(字节数组)是Python中处理二进制数据的两个核心类型。它们是字符编码的"幕后英雄"——你通过网络发送的每一条消息、从硬盘读取的每一个文件,在最底层都是字节流。
很多Python初学者对bytes感到困惑:它和str有什么区别?为什么有时候会报 TypeError: a bytes-like object is required?什么时候该用bytes,什么时候该用str?这篇文章将把这些问题一一讲透。
二、为什么需要bytes——字符、编码、字节的关系
2.1 从字符到字节
计算机只能存储0和1(即比特,bit)。8个比特组成一个字节(byte),一个字节可以表示256种状态(0-255)。人类使用的文字符号需要被"编码"为字节序列才能在计算机中存储和传输——这就是字符编码。
# 同一个字符在不同编码下对应不同的字节序列
char = '中'
# UTF-8 编码:3个字节
utf8_bytes = char.encode('utf-8')
print(utf8_bytes) # b'\xe4\xb8\xad'
print(len(utf8_bytes)) # 3
# GBK 编码:2个字节
gbk_bytes = char.encode('gbk')
print(gbk_bytes) # b'\xd6\xd0'
print(len(gbk_bytes)) # 2
# UTF-16 编码:2个字节(BMP字符)
utf16_bytes = char.encode('utf-16')
print(utf16_bytes) # b'\xff\xfe-N'(包含BOM)
# ASCII 编码:不支持中文
try:
ascii_bytes = char.encode('ascii')
except UnicodeEncodeError as e:
print(f'错误:{e}')
# 错误:'ascii' codec can't encode character '中'
关键认知:str是"人类可读的字符序列",bytes是"机器可读的字节序列"。两者通过编码(encode)和解码(decode)相互转换。这个关系是理解Python文本处理的基础。
2.2 str和bytes的本质区别
# str:Unicode字符序列
text = 'Python编程'
print(type(text)) # <class 'str'>
print(len(text)) # 8(8个字符)
print(text[0]) # P(第一个字符)
print(text[6]) # 编(第七个字符)
# bytes:0-255的整数序列
data = text.encode('utf-8')
print(type(data)) # <class 'bytes'>
print(len(data)) # 12(12个字节:6个英文+2×3个中文)
print(data[0]) # 80(整数,不是字符!)
print(data[6]) # 231(整数,不是字符!)
# str的每个元素是长度为1的str
# bytes的每个元素是0-255的int
for ch in text[:3]:
print(type(ch), ch) # <class 'str'> P, y, t
for byte in data[:3]:
print(type(byte), byte) # <class 'int'> 80, 121, 116
三、bytes——不可变的字节序列
3.1 创建bytes的四种方式
# 方式一:用b前缀的字符串字面量(仅限ASCII字符)
data1 = b'hello'
data2 = b'abc123!@#'
print(data1) # b'hello'
print(type(data1)) # <class 'bytes'>
# b前缀的字符串只能包含ASCII字符和转义序列
# data3 = b'你好' # SyntaxError!中文不能直接写在b''中
# 但可以用转义序列:
data3 = b'\xe4\xbd\xa0\xe5\xa5\xbd' # UTF-8编码的"你好"
print(data3.decode('utf-8')) # 你好
# 方式二:用bytes()构造函数从整数序列创建
data4 = bytes([72, 101, 108, 108, 111]) # ASCII码
print(data4) # b'Hello'
data5 = bytes([0, 1, 2, 127, 128, 255])
print(data5) # b'\x00\x01\x02\x7f\x80\xff'
# 整数必须在0-255范围内
# bytes([256]) # ValueError: bytes must be in range(0, 256)
# 方式三:用bytes()构造函数指定长度(全零)
data6 = bytes(10)
print(data6) # b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
# 方式四:通过encode()方法从str创建
data7 = 'Python编程'.encode('utf-8')
print(data7) # b'Python\xe7\xbc\x96\xe7\xa8\x8b'
data8 = 'Python编程'.encode('gbk')
print(data8) # b'Python\xb1\xe0\xb3\xcc'
3.2 bytes的基本操作
data = b'hello world'
# 索引访问——返回整数(不是bytes!)
print(data[0]) # 104('h'的ASCII码)
print(data[1]) # 101
print(data[-1]) # 100
# 切片——返回bytes
print(data[:5]) # b'hello'
print(data[6:]) # b'world'
print(data[::-1]) # b'dlrow olleh'
# 长度
print(len(data)) # 11
# 成员检查(可以用整数或单字节的bytes)
print(104 in data) # True(检查字节值104 = 'h')
print(b'h' in data) # True(检查子序列b'h')
print(b'hello' in data) # True
print(b'Python' in data) # False
# 遍历——每次拿到整数
for byte in b'ABC':
print(byte, end=' ') # 65 66 67
print()
# 转化为列表
print(list(b'ABC')) # [65, 66, 67]
3.3 bytes的常用方法
bytes支持大部分str的方法,但操作对象是字节而非字符:
data = b'hello world hello python' # 查找 print(data.find(b'hello')) # 0 print(data.rfind(b'hello')) # 12(第二次出现) print(data.find(b'java')) # -1(未找到) print(data.index(b'world')) # 6 # print(data.index(b'java')) # ValueError print(data.count(b'hello')) # 2 print(data.startswith(b'hel')) # True print(data.endswith(b'ton')) # True # 分割 print(data.split()) # [b'hello', b'world', b'hello', b'python'] print(data.split(b' ')) # 同上 print(data.partition(b'world'))# (b'hello ', b'world', b' hello python') # 大小写转换 print(b'HELLO'.lower()) # b'hello' print(b'hello'.upper()) # b'HELLO' print(b'hello world'.title()) # b'Hello World' print(b'Hello'.swapcase()) # b'hELLO' # 替换 print(data.replace(b'hello', b'hi')) # b'hi world hi python' print(data.replace(b'hello', b'hi', 1)) # b'hi world hello python' # 去除空白 data2 = b' hello ' print(data2.strip()) # b'hello' print(data2.lstrip()) # b'hello ' print(data2.rstrip()) # b' hello' # 判断 print(b'abc123'.isalnum()) # True print(b'abc'.isalpha()) # True print(b'123'.isdigit()) # True print(b' '.isspace()) # True print(b'HELLO'.isupper()) # True print(b'hello'.islower()) # True # 连接 parts = [b'hello', b'world', b'python'] print(b' '.join(parts)) # b'hello world python'
四、bytearray——可变的字节序列
4.1 基本概念
bytearray 是 bytes 的可变版本。你可以原地修改它,而不需要创建新的对象。这在需要频繁修改字节数据的场景中非常有用(如网络缓冲区的累积、图像数据的处理)。
# 创建bytearray——和bytes类似的方式
ba1 = bytearray(b'hello')
ba2 = bytearray([72, 101, 108, 108, 111])
ba3 = bytearray(10) # 10个零字节
ba4 = bytearray('Python编程', 'utf-8')
print(ba1) # bytearray(b'hello')
print(type(ba1)) # <class 'bytearray'>
# 关键区别:bytearray可以原地修改
ba1[0] = 72 # 修改第一个字节(已经是H,没有变化)
ba1[0] = 106 # 改为'j'的ASCII码(106)
print(ba1) # bytearray(b'jello')
# bytes则不行
# data = b'hello'
# data[0] = 106 # TypeError: 'bytes' object does not support item assignment
4.2 bytearray的修改操作
ba = bytearray(b'hello world')
# 索引赋值
ba[0] = ord('H')
print(ba) # bytearray(b'Hello world')
# 切片赋值
ba[6:11] = b'python'
print(ba) # bytearray(b'Hello python')
# 切片删除
ba[5:11] = b''
print(ba) # bytearray(b'Hello')
# 追加
ba.append(33) # 感叹号
print(ba) # bytearray(b'Hello!')
# 扩展
ba.extend(b' world')
print(ba) # bytearray(b'Hello! world')
# 插入
ba.insert(5, ord(','))
print(ba) # bytearray(b'Hello,! world')
# 删除指定位置的字节
del ba[5] # 删除逗号
print(ba) # bytearray(b'Hello! world')
# 删除切片
del ba[5:10]
print(ba) # bytearray(b'Hello')
# 弹出最后一个字节
last = ba.pop()
print(last) # 111('o'的ASCII码)
print(ba) # bytearray(b'Hell')
# 弹出指定位置
first = ba.pop(0)
print(first) # 72
print(ba) # bytearray(b'ell')
# 移除第一个匹配的字节值
ba2 = bytearray(b'abracadabra')
ba2.remove(ord('a'))
print(ba2) # bytearray(b'bracadabra')
# 反转
ba2.reverse()
print(ba2) # bytearray(b'arbadacarb')
# 清空
ba2.clear()
print(ba2) # bytearray(b'')
bytearray提供了类似list的修改接口,同时保持了bytes的字节序列特性。它的每个元素仍然是0-255的整数。
4.3 bytearray的其他方法
ba = bytearray(b'Hello World')
# copy方法(bytearray特有)
ba_copy = ba.copy()
print(ba_copy) # bytearray(b'Hello World')
ba[0] = ord('h')
print(ba) # bytearray(b'hello World')
print(ba_copy) # bytearray(b'Hello World')(不受影响)
# 其他和bytes相同的方法
print(ba.find(b'ello')) # 1
print(ba.replace(b'hello', b'HELLO'))
print(ba.lower())
print(ba.upper())
# 转换为bytes(得到一个不可变的拷贝)
immutable = bytes(ba)
print(type(immutable)) # <class 'bytes'>
print(immutable) # b'hello World'
五、编码与解码——str和bytes之间的桥梁
5.1 encode()——从str到bytes
text = 'Python编程'
# 各种编码方式
print(text.encode('utf-8')) # b'Python\xe7\xbc\x96\xe7\xa8\x8b'
print(text.encode('gbk')) # b'Python\xb1\xe0\xb3\xcc'
print(text.encode('utf-16')) # b'\xff\xfeP\x00y\x00...'
print(text.encode('utf-16-le')) # b'P\x00y\x00...'(无BOM,小端)
print(text.encode('utf-16-be')) # b'\x00P\x00y...'(无BOM,大端)
print(text.encode('iso-8859-1', errors='replace')) # 不支持中文用?替代
# encode的errors参数
text = 'Python编程¥€'
# strict(默认):遇到不能编码的字符抛出异常
# text.encode('ascii') # UnicodeEncodeError
# ignore:忽略不能编码的字符
print(text.encode('ascii', errors='ignore')) # b'Python'
# replace:用?替换不能编码的字符
print(text.encode('ascii', errors='replace')) # b'Python????'
# xmlcharrefreplace:用XML字符引用替换
print(text.encode('ascii', errors='xmlcharrefreplace'))
# b'Python编程¥¬'
# backslashreplace:用反斜杠转义替换
print(text.encode('ascii', errors='backslashreplace'))
# b'Python\\u7f16\\u7a0b\\u00a5\\u20ac'
# namereplace:用\N{NAME}替换
print(text.encode('ascii', errors='namereplace'))
# b'Python\\N{CJK UNIFIED IDEOGRAPH-7F16}...'
5.2 decode()——从bytes到str
# 基本解码
data = b'Python\xe7\xbc\x96\xe7\xa8\x8b'
text = data.decode('utf-8')
print(text) # Python编程
# 换一种编码方式解码同一个数据——得到乱码!
try:
text_gbk = data.decode('gbk')
print(text_gbk) # Python缂栫▼(乱码!)
except:
pass
# 正确的做法是知道原始编码
data_gbk = 'Python编程'.encode('gbk')
text_gbk = data_gbk.decode('gbk')
print(text_gbk) # Python编程(正确还原)
# decode的errors参数(和encode类似)
corrupted = b'hello\xffworld'
# strict(默认)
# corrupted.decode('utf-8') # UnicodeDecodeError
# ignore:忽略无法解码的字节
print(corrupted.decode('utf-8', errors='ignore')) # helloworld
# replace:用�(U+FFFD)替换无法解码的字节
print(corrupted.decode('utf-8', errors='replace')) # hello�world
# 实际场景:处理未知编码的文本
def safe_decode(data):
"""尝试用多种常见编码解码,返回最可能的结果"""
encodings = ['utf-8', 'gbk', 'gb2312', 'iso-8859-1', 'windows-1252']
for enc in encodings:
try:
decoded = data.decode(enc)
# 检查是否包含大量乱码字符
if decoded.count('') / max(len(decoded), 1) < 0.1:
return enc, decoded
except (UnicodeDecodeError, UnicodeError):
continue
# 最后的备选方案
return 'utf-8', data.decode('utf-8', errors='replace')
# 测试
for text in ['Python编程', 'Hello World 你好', 'こんにちは']:
data = text.encode('utf-8')
enc, decoded = safe_decode(data)
print(f'编码检测: {enc}, 内容: {decoded}')
5.3 BOM(字节顺序标记)是什么
BOM(Byte Order Mark)是Unicode文件开头的特殊标记,用于标识字节序和编码方式:
# UTF-8 BOM: EF BB BF
bom_utf8 = b'\xef\xbb\xbf'
print('hello'.encode('utf-8-sig')) # b'\xef\xbb\xbfhello'
print('hello'.encode('utf-8')) # b'hello'(无BOM)
# UTF-16 BOM: FE FF(大端)或 FF FE(小端)
print('hello'.encode('utf-16')) # b'\xff\xfeh\x00e\x00l\x00l\x00o\x00'
# FF FE = 小端(Little Endian)
# 处理带BOM的文件
def read_file_with_bom(filepath):
"""读取可能带BOM的文本文件"""
with open(filepath, 'rb') as f:
raw = f.read()
# 检测并移除BOM
if raw.startswith(b'\xef\xbb\xbf'):
return raw[3:].decode('utf-8')
elif raw.startswith(b'\xff\xfe'):
return raw[2:].decode('utf-16-le')
elif raw.startswith(b'\xfe\xff'):
return raw[2:].decode('utf-16-be')
else:
# 默认尝试UTF-8
return raw.decode('utf-8')
# 最简单的方式:用utf-8-sig编码自动处理BOM
with open('test_bom.txt', 'w', encoding='utf-8-sig') as f:
f.write('带BOM的文字')
# 使用encoding='utf-8-sig'读取时会自动跳过BOM
六、bytes和bytearray的实用场景
6.1 文件I/O——二进制模式
# 文本模式读写(自动编码/解码)
with open('test.txt', 'w', encoding='utf-8') as f:
f.write('Python编程')
with open('test.txt', 'r', encoding='utf-8') as f:
content = f.read() # 返回str
print(type(content)) # <class 'str'>
# 二进制模式读写(直接操作bytes)
with open('test.txt', 'wb') as f:
f.write('Python编程'.encode('utf-8'))
with open('test.txt', 'rb') as f:
data = f.read() # 返回bytes
print(type(data)) # <class 'bytes'>
print(data) # b'Python\xe7\xbc\x96\xe7\xa8\x8b'
# 处理非文本文件(图片、音频、视频等)
def copy_file_binary(src, dst):
"""以二进制模式复制文件——适用于任何文件类型"""
with open(src, 'rb') as f_in:
with open(dst, 'wb') as f_out:
# 分块读取,避免大文件占满内存
while True:
chunk = f_in.read(8192) # 每次读8KB
if not chunk:
break
f_out.write(chunk)
# 检查文件是否是特定类型(通过文件头魔数)
def detect_file_type(filepath):
"""通过文件头的魔数(magic bytes)检测文件类型"""
magic_bytes = {
b'\x89PNG\r\n\x1a\n': 'PNG图片',
b'\xff\xd8\xff': 'JPEG图片',
b'GIF87a': 'GIF87a图片',
b'GIF89a': 'GIF89a图片',
b'%PDF': 'PDF文档',
b'PK\x03\x04': 'ZIP压缩包',
b'\x50\x4b\x03\x04': 'DOCX/XLSX文档',
}
with open(filepath, 'rb') as f:
header = f.read(8) # 读取前8个字节
for magic, file_type in magic_bytes.items():
if header.startswith(magic):
return file_type
return '未知类型'
# 例:detect_file_type('example.pdf') → 'PDF文档'
6.2 网络编程
import socket
# 发送和接收的都是bytes
def simple_http_get(host, path='/'):
"""用原始socket发送HTTP GET请求"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, 80))
# HTTP请求是bytes
request = (
f'GET {path} HTTP/1.1\r\n'
f'Host: {host}\r\n'
f'Connection: close\r\n'
f'\r\n'
).encode('utf-8')
sock.sendall(request)
# 接收响应(bytes)
response = b''
while True:
chunk = sock.recv(4096)
if not chunk:
break
response += chunk
sock.close()
return response
# 构建自定义二进制协议的数据包
import struct
def build_packet(msg_id, msg_type, payload):
"""构建一个简单的二进制协议数据包"""
# 协议格式:
# 魔数(2B) + 消息ID(4B) + 消息类型(2B) + 载荷长度(2B) + 载荷(NB)
magic = b'\xAB\xCD'
payload_bytes = payload.encode('utf-8')
header = struct.pack('!IHH', msg_id, msg_type, len(payload_bytes))
packet = magic + header + payload_bytes
return packet
def parse_packet(packet):
"""解析数据包"""
magic = packet[:2]
if magic != b'\xAB\xCD':
raise ValueError('无效的魔数')
header = packet[2:10]
msg_id, msg_type, payload_len = struct.unpack('!IHH', header)
payload = packet[10:10 + payload_len].decode('utf-8')
return {
'msg_id': msg_id,
'msg_type': msg_type,
'payload': payload,
}
packet = build_packet(1, 100, 'hello server')
print(packet)
parsed = parse_packet(packet)
print(parsed) # {'msg_id': 1, 'msg_type': 100, 'payload': 'hello server'}
6.3 使用struct模块打包和解包二进制数据
import struct
# struct用于在Python值和C结构体之间转换
# 格式字符:b(byte), h(short), i(int), f(float), d(double), s(char[])
# 打包数据
packed = struct.pack('i f 10s', 42, 3.14, b'hello')
print(packed)
print(len(packed)) # 4 + 4 + 10 = 18字节
# 解包数据
unpacked = struct.unpack('i f 10s', packed)
print(unpacked) # (42, 3.140000104904175, b'hello\x00\x00\x00\x00\x00')
# 大端和小端
# > 表示大端(网络字节序)
# < 表示小端(Intel x86架构)
# ! 表示网络字节序(大端)
# 网络数据通常使用大端
big_endian = struct.pack('>I', 0x12345678)
print(big_endian) # b'\x124Vx'
print(' '.join(f'{b:02x}' for b in big_endian)) # 12 34 56 78
little_endian = struct.pack('<I', 0x12345678)
print(' '.join(f'{b:02x}' for b in little_endian)) # 78 56 34 12
# 解析二进制文件头(例如BMP图片头)
def parse_bmp_header(filepath):
"""解析BMP文件头"""
with open(filepath, 'rb') as f:
# BMP文件头结构(前14字节)
signature = f.read(2) # 'BM'(2字节)
file_size = struct.unpack('<I', f.read(4))[0] # 文件大小(4字节)
reserved = f.read(4)
data_offset = struct.unpack('<I', f.read(4))[0] # 数据偏移(4字节)
# DIB头(接下来的4字节标识头大小)
dib_size = struct.unpack('<I', f.read(4))[0]
width = struct.unpack('<i', f.read(4))[0] # 宽度
height = struct.unpack('<i', f.read(4))[0] # 高度
planes = struct.unpack('<H', f.read(2))[0] # 色彩平面数
bit_count = struct.unpack('<H', f.read(2))[0] # 每像素位数
return {
'signature': signature,
'file_size': file_size,
'data_offset': data_offset,
'width': width,
'height': height,
'planes': planes,
'bit_depth': bit_count,
}
七、bytes、bytearray、str三者的关系
7.1 互相转换
# 完整的转换图
# str → bytes(编码,encode)
text = 'Python编程'
data = text.encode('utf-8') # str → bytes
# bytes → str(解码,decode)
text2 = data.decode('utf-8') # bytes → str
# str → bytearray(先编码再用构造函数)
ba = bytearray('Python编程', 'utf-8') # str → bytearray
# bytearray → str(解码)
text3 = ba.decode('utf-8') # bytearray → str
# bytes → bytearray(构造函数或copy)
ba2 = bytearray(data) # bytes → bytearray
# bytearray → bytes(构造函数或bytes转换)
data2 = bytes(ba) # bytearray → bytes
# 总结转换关系
# str ←encode/decode→ bytes ←bytearray()/bytes()→ bytearray
# str ←bytearray()/.decode()→ bytearray
7.2 序列类型对比
# Python三种序列类型对比
s = 'Python' # str:不可变Unicode字符序列
b = b'Python' # bytes:不可变字节序列(0-255)
ba = bytearray(b'Python') # bytearray:可变字节序列(0-255)
# 共同的序列操作
for item in s: print(item) # 遍历——str类型
for item in b: print(item) # 遍历——int类型
for item in ba: print(item) # 遍历——int类型
# 索引
print(s[0]) # 'P'(str)
print(b[0]) # 80(int)
print(ba[0]) # 80(int)
# 切片
print(s[:3]) # 'Pyt'(str)
print(b[:3]) # b'Pyt'(bytes)
print(ba[:3]) # bytearray(b'Pyt')(bytearray)
# 成员检查
print('y' in s) # True(查字符)
print(b'y' in b) # True(查单字节bytes)
print(121 in b) # True(查整数)
print(121 in ba) # True
# 可变性
# s[0] = 'J' # TypeError
# b[0] = 74 # TypeError
ba[0] = 74 # OK!变成 bytearray(b'Jython')
7.3 性能特性
import time
# bytes vs bytearray 性能对比
size = 1000000
# bytes 不可变——每次修改都需要创建新对象
start = time.perf_counter()
data = b'x' * size
# 如果要"修改",只能创建新的bytes
data = data[:500000] + b'y' + data[500001:]
print(f'bytes修改耗时: {time.perf_counter() - start:.4f}秒')
# bytearray 可变——原地修改
start = time.perf_counter()
ba = bytearray(b'x' * size)
ba[500000] = ord('y') # 原地修改,不需要复制
print(f'bytearray修改耗时: {time.perf_counter() - start:.4f}秒')
# bytearray修改后者比bytes快得多
# 因为bytes的"修改"实际是创建了新对象并复制整个数据
# 而bytearray只修改了一个字节
# bytes的优势:由于不可变,bytes可以用作字典的键
d = {b'hello': 'world', b'key': 'value'}
print(d[b'hello']) # world
# bytearray由于可变,不能用做字典键
# d = {bytearray(b'hello'): 'world'} # TypeError: unhashable type
八、内存中的字节表示
8.1 bytes的显示规则
# bytes在显示时,可打印的ASCII字符显示为字符本身
# 不可打印的字节能显示为\xhh形式
data = bytes([65, 66, 67, 0, 10, 255, 128, 32])
print(data) # b'ABC\x00\n\xff\x80 '
# 显示的规则:
# 1. 可打印ASCII(32-126)→显示为字符
# 2. 常用转义字符(\n, \r, \t等)→显示为转义序列
# 3. 其他→显示为\xhh(十六进制)
# 自定义格式化显示
def format_bytes(data, bytes_per_line=16):
"""以十六进制dump形式显示bytes(类似hexdump)"""
result = []
for i in range(0, len(data), bytes_per_line):
chunk = data[i:i + bytes_per_line]
hex_part = ' '.join(f'{b:02x}' for b in chunk)
# 补齐不足的行
hex_part = hex_part.ljust(bytes_per_line * 3 - 1)
ascii_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
result.append(f'{i:08x} {hex_part} |{ascii_part}|')
return '\n'.join(result)
data = b'Python\xe7\xbc\x96\xe7\xa8\x8b is great!\n\x00\xff\x80'
print(format_bytes(data))
# 00000000 50 79 74 68 6f 6e e7 bc 96 e7 a8 8b 20 69 73 20 |Python...... is |
# 00000010 67 72 65 61 74 21 0a 00 ff 80 |great!.... |
8.2 字节序(Endianness)
# 字节序:多字节数据在内存中的存储顺序
# 例如:0x12345678(4字节整数)
value = 0x12345678
# 大端(Big Endian):最高有效字节存储在最低地址
big = value.to_bytes(4, 'big')
print('大端:', ' '.join(f'{b:02x}' for b in big)) # 12 34 56 78
# 小端(Little Endian):最低有效字节存储在最低地址
little = value.to_bytes(4, 'little')
print('小端:', ' '.join(f'{b:02x}' for b in little)) # 78 56 34 12
# 从bytes还原整数
print(int.from_bytes(big, 'big')) # 305419896 (= 0x12345678)
print(int.from_bytes(little, 'little')) # 305419896 (= 0x12345678)
# 用错字节序
print(int.from_bytes(big, 'little')) # 2018915346(错误的值)
# 网络协议通常使用大端(Network Byte Order)
# x86/x64 CPU使用小端
# ARM可以配置为大端或小端
# 判断当前系统字节序
import sys
print(sys.byteorder) # little(在x86/x64 Windows/Linux上)
九、实战:从网络下载到文件保存
import urllib.request
import hashlib
import struct
def download_and_verify(url, output_path, expected_sha256=None):
"""下载文件,用bytearray累积数据,并可选地验证SHA256哈希"""
response = urllib.request.urlopen(url)
# 用bytearray累积下载的数据
buffer = bytearray()
# 下载为主
total_size = int(response.headers.get('Content-Length', 0))
# hashlib可以与bytearray直接配合使用
hasher = hashlib.sha256()
chunk_size = 8192
downloaded = 0
while True:
chunk = response.read(chunk_size)
if not chunk:
break
# 累积到bytearray(比bytes拼接高效)
buffer.extend(chunk)
hasher.update(chunk)
downloaded += len(chunk)
if total_size > 0:
percent = downloaded / total_size * 100
print(f'\r下载进度:{percent:.1f}%({downloaded}/{total_size})',
end='', flush=True)
print()
# 验证哈希
actual_hash = hasher.hexdigest()
if expected_sha256:
if actual_hash == expected_sha256:
print('✅ SHA256校验通过')
else:
print(f'❌ SHA256校验失败!预期:{expected_sha256}')
print(f' 实际:{actual_hash}')
return False
# 写入文件
with open(output_path, 'wb') as f:
f.write(buffer)
print(f'文件已保存到:{output_path}({len(buffer):,} 字节)')
return True
def build_simple_image():
"""用bytearray构建一个简单的BMP图片文件"""
width, height = 100, 60
# 构建像素数据——一个简单的蓝色到红色的渐变
pixels = bytearray()
for y in range(height):
for x in range(width):
r = int(255 * x / width) # 红色从左到右递增
g = int(255 * y / height) # 绿色从上到下递增
b_val = 255 - (r + g) // 2 # 蓝色补足
# BMP中像素存储顺序是BGR
pixels.extend([b_val, g, r])
# BMP要求每行对齐到4字节
row_size = (width * 3 + 3) // 4 * 4
padding = row_size - width * 3
padded_pixels = bytearray()
for y in range(height):
start = y * width * 3
end = start + width * 3
padded_pixels.extend(pixels[start:end])
padded_pixels.extend(b'\x00' * padding)
# BMP文件头(14字节)+ DIB头(40字节)
file_size = 14 + 40 + len(padded_pixels)
header = struct.pack(
'<2sIHHIIiiHHIIIIII',
b'BM', # 签名
file_size, # 文件大小
0, 0, # 保留
54, # 数据偏移(14+40=54)
40, # DIB头大小
width, height, # 宽高
1, # 色彩平面
24, # 位深度(RGB=24位)
0, # 压缩方式(无压缩)
len(padded_pixels), # 图像大小
2835, 2835, # 水平/垂直分辨率(72 DPI)
0, 0 # 调色板颜色数(全部)、重要颜色数
)
return bytes(header) + bytes(padded_pixels)
# 构建一张简单的渐变图
bmp_data = build_simple_image()
with open('gradient.bmp', 'wb') as f:
f.write(bmp_data)
print(f'生成了渐变图:{len(bmp_data):,} 字节')
十、常见错误与调试
10.1 经典错误
# 错误一:把str传给需要bytes的函数
# hashlib.sha256('hello') # TypeError
import hashlib
h = hashlib.sha256(b'hello') # 正确
# 错误二:b''中放置非ASCII字符
# data = b'你好' # SyntaxError
data = '你好'.encode('utf-8') # 正确
# 错误三:混淆bytes和str的拼接
text = 'hello'
binary = b'world'
# result = text + binary # TypeError: can only concatenate str to str
# 解决:统一类型
result = text + binary.decode('utf-8') # 都转为str
result2 = text.encode() + binary # 都转为bytes
# 错误四:忘记指定编码
# 在不同平台上默认编码可能不同
# 永远显式指定encoding参数
with open('file.txt', 'w', encoding='utf-8') as f:
f.write('内容')
with open('file.txt', 'r', encoding='utf-8') as f:
content = f.read()
# 错误五:bytearray的index方法返回值和str的不同含义
s = 'hello'
b = b'hello'
print(s.find('h')) # 0(字符索引)
print(b.find(b'h')) # 0(字节索引)
# 但对于多字节字符,字节索引和字符索引可能不同
s2 = '你好世界'
b2 = s2.encode('utf-8')
print(s2.find('世')) # 2(第3个字符)
print(b2.find('世'.encode('utf-8'))) # 6(第7个字节,因为每个中文3字节)
10.2 bytes和十六进制字符串的互转
# bytes → 十六进制字符串
data = b'hello\xff\x00\x80'
hex_str = data.hex()
print(hex_str) # 68656c6c6fff0080
# 带分隔符的十六进制
hex_spaced = data.hex(' ')
print(hex_spaced) # 68 65 6c 6c 6f ff 00 80
hex_colon = data.hex(':')
print(hex_colon) # 68:65:6c:6c:6f:ff:00:80(MAC地址格式)
# 十六进制字符串 → bytes
original = bytes.fromhex('68656c6c6fff0080')
print(original) # b'hello\xff\x00\x80'
# 以下也是合法的
print(bytes.fromhex('68 65 6C 6C 6F')) # b'hello'(跳过空格,不区分大小写)
print(bytes.fromhex('6865 6C6C 6F')) # b'hello'(空格位置任意)
# 实际应用:生成随机会话令牌
import secrets
token = secrets.token_hex(32) # 64字符的十六进制字符串 = 32字节
print(f'随机会话令牌: {token}')
# 或直接用bytes
token_bytes = secrets.token_bytes(32)
print(f'字节令牌: {token_bytes.hex()}')
十一、本篇小结
bytes和bytearray是Python二进制数据处理的双子星:
- bytes:不可变字节序列,类似"字节版的str"。适合网络协议、文件I/O、哈希计算等场景
- bytearray:可变字节序列,类似"字节版的list"。适合需要频繁修改字节数据的场景(缓冲区的累积与修改)
- 编码与解码:
str.encode()将字符转为字节,bytes.decode()将字节转为字符。处理不同编码时需要显式指定encoding参数 - struct模块:在Python值和C结构体之间转换,是解析二进制协议的核心工具
- 核心认知:str是"人类可读的字符序列",bytes是"机器可读的字节序列"。两者分工明确,各司其职
理解bytes不是可选项,而是Python开发者必备的能力。当你开始处理文件上传下载、网络编程、加密解密、数据序列化时,bytes将是你最频繁打交道的类型。下一篇我们进入Python中最重要的容器类型——列表list的创建与基本操作。
到此这篇关于Python基础指南之字节类型bytes与字节数组bytearray区别详解的文章就介绍到这了,更多相关Python bytes与bytearray内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!


最新评论