Python中的"字节串"(`bytes`数据类型)是什么?

68 投票
4 回答
89422 浏览
提问于 2025-04-18 01:05

在Python中,什么是“字节串”?bytes类型是什么,它是如何工作的呢?

我的理解是,普通的“ASCII字符串”存储的是一系列“字符”,这些字符的“ASCII值”范围是0到255,每个数字代表一个字符。同样,我知道Unicode使用8位或16位来表示每个字符。

举个更清楚的例子:假设我执行了

>>> 'a'.encode()
b'a'

好的,结果是一个bytes,它存储了一个字节。

不过,我听说bytes表示的是一个不可变的字节序列,没有特定的解释。那么……我为什么能读到“a”呢

如果我用命令行查看这个字符的ASCII值:

$ printf "%d\n" "'a"
97

这就有点道理了。如果我们把数字97当作ASCII来看,那么我们就得到了字母a。同样,这个值用二进制表示,扩展到8位,就是01100001

那么,为什么'a'.encode()看起来像b'a'而不是b'97',或者b'01100001'(底层的位模式)?为什么它看起来和ASCII解释的结果一样?

说到这个,如果我把一个bytes写入以二进制模式打开的文件:

with open('testbytestring.txt', 'wb') as f:
    f.write(b'helloworld')

我在文件中仍然能看到人类可读的文本helloworld!这是为什么呢?

4 个回答

1

字节对象是一种不可改变的单字节序列。文档里对它们是什么以及如何使用有很好的解释。

9

顾名思义,Python 2/3 中的 bytes(在 Python 2.7 中也可以简单称为 str)就是一串 字节。而且,正如其他人所提到的,它是不可变的,也就是说一旦创建就不能更改。

它和 Python 3 中的 str(更准确地说,在 Python 2.7 中是 unicode)是不同的。str 是一串 抽象的 Unicode 字符(也叫 UTF-32,不过 Python 3 在底层做了一些优化,使得实际占用的内存更少,类似于 UTF-8,甚至可能更通用)。

基本上,有三种方式来“解释”这些字节。你可以查看某个元素的数字值,像这样:

>>> ord(b'Hello'[0])  # Python 2.7 str
72
>>> b'Hello'[0]  # Python 3 bytestring
72

或者你可以告诉 Python 将一个或多个元素以 8位字符 的形式输出到终端(或者文件、设备、套接字等),像这样:

>>> print b'Hello'[0] # Python 2.7 str or bytes
H
>>> import sys
>>> sys.stdout.buffer.write(b'Hello'[0:1]) and None; print() # Python 3 bytes
H

正如 Jack 提到的,在这种情况下,是 你的终端 在解释字符,而不是 Python。

最后,正如你在自己的研究中看到的,你也可以让 Python 来解释 bytes。例如,在 Python 2.7 中,你可以这样构建一个抽象的 unicode 对象:

>>> u1234 = unicode(b'\xe1\x88\xb4', 'utf-8')
>>> print u1234.encode('utf-8') # if terminal supports UTF-8
ሴ
>>> u1234
u'\u1234'
>>> print ('%04x' % ord(u1234))
1234
>>> type(u1234)
<type 'unicode'>
>>> len(u1234)
1
>>>

在 Python 3 中可以这样做:

>>> u1234 = str(b'\xe1\x88\xb4', 'utf-8')
>>> print (u1234) # if terminal supports UTF-8 AND python auto-infers
ሴ
>>> u1234.encode('unicode-escape')
b'\\u1234'
>>> print ('%04x' % ord(u1234))
1234
>>> type(u1234)
<class 'str'>
>>> len(u1234)
1

(我相信 Python 2.7 和 Python 3 之间在字节串、字符串和 Unicode 方面的语法差异,可能与 Python 2.7 的持续受欢迎有关。我想当 Python 3 被创造出来时,他们还没有意识到一切都会变成 UTF-8,因此关于抽象的讨论是多余的)。

但是,如果你不想让 Unicode 抽象化,这个过程不会自动发生。bytes 的意义在于你可以直接访问字节。即使你的字符串恰好是一个 UTF-8 序列,你仍然可以访问序列中的字节:

>>> len(b'\xe1\x88\xb4')
3
>>> b'\xe1\x88\xb4'[0]
'\xe1'

这在 Python 2.7 和 Python 3 中都适用,区别在于在 Python 2.7 中你有 strbytes,而在 Python 3 中你只有 bytes

你还可以用 bytes 做其他很棒的事情,比如知道它们是否能适合文件中的保留空间,直接通过套接字发送它们,正确计算 HTTP 的 content-length 字段,以及避免 Python Bug 8260。简而言之,当你的数据以字节形式处理和存储时,使用 bytes

40

很多人误以为文本就是ASCII、UTF-8或者Windows-1252,因此字节就是文本。

其实,文本就像图像一样,文本就是文本。把文本或图像存储到磁盘上,其实是把这些数据编码成一串字节。编码图像成字节的方法有很多,比如JPEGPNGSVG,而编码文本的方法也有很多,比如ASCII、UTF-8或Windows-1252。

一旦编码完成,字节就只是字节了。字节不再是图像,它们忘记了自己代表的颜色,虽然图像格式解码器可以恢复这些信息。字节同样也忘记了它们曾经是哪些字母。实际上,字节根本不记得自己是图像还是文本。只有一些外部的信息(比如文件名、媒体头等)才能猜测这些字节应该代表什么,甚至这些信息也可能是错误的(比如数据损坏的情况下)。

所以,在Python(Python 3)中,我们有两种看起来可能相似的类型;对于文本,我们有str,它知道自己是文本,知道自己应该代表哪些字母。它不知道这些字母可能对应哪些字节,因为字母和字节是不同的。我们还有bytestring,它不知道自己是文本、图像还是其他类型的数据。

这两种类型表面上看起来相似,因为它们都是一系列的东西,但它们所包含的东西却大不相同。

在实现上,str在内存中以UCS-?的形式存储,其中的?是由实现决定的,可能是UCS-4、UCS-2或UCS-1,这取决于编译时的选项和表示字符串中包含的码点


“但为什么呢?”

一些看起来像文本的东西实际上是用其他方式定义的。一个很好的例子就是世界上许多互联网协议。例如,HTTP是一个“文本”协议,但实际上是用ABNF语法定义的,这种语法在RFC中很常见。这些协议是用八位字节来表达的,而不是字符,尽管也可能会建议一种非正式的编码方式:

2.3. 终端值

规则解析为一串终端值,有时称为字符。在ABNF中,字符仅仅是一个非负整数。在某些情况下,会指定将值映射(编码)到字符集(如ASCII)中的特定方式。

这个区别很重要,因为在互联网上无法发送文本,唯一能做的就是发送字节。说“文本但用‘foo’编码”会让格式变得更加复杂,因为客户端和服务器需要自己搞清楚编码的问题,希望它们能以相同的方式处理,因为最终它们必须以字节的形式传递数据。这是双重无用,因为这些协议通常根本不涉及文本处理,只是为了方便实现者。服务器所有者和最终用户都不关心看到Transfer-Encoding: chunked这些字眼,只要服务器和浏览器能正确理解就行。

相比之下,当处理文本时,你并不在乎它是如何编码的。你可以用任何方式表达“Heävy Mëtal Ümlaüts”,除了“Heδvy Mλtal άmlaόts”。


因此,这两种不同的类型让你可以明确地表示“这个值是文本”或“这个值是字节”。

33

Python 并不知道如何表示字节串。这就是重点。

当你在几乎任何输出窗口中输出值为97的字符时,你会看到字符'a',但这并不是实现的一部分;这只是一个在本地成立的事实。如果你想要编码,就不能使用字节串。如果你使用字节串,就没有编码。

你提到的.txt文件说明你对发生的事情有些误解。你看,普通文本文件也没有编码。它们只是一系列字节。这些字节会被文本编辑器翻译成字母,但如果你使用的字符超出了常见的ASCII字符范围,完全没有保证其他人打开你的文件时会看到和你一样的内容。

撰写回答