14. 字符串、字节和 Unicode
14. 字符串、字节和 Unicode
文本和二进制数据是 Python 中独立的对象族。str代表 Unicode 文本。bytes表示不可变的二进制数据。bytearray表示可变的二进制数据。
这种分离是 Python 3 最重要的运行时设计选择之一。文本具有字符和编码。二进制数据有字节。 CPython 使用不同的对象布局、API 和不变量来实现这些概念。
14.1 文本与二进制数据
字符串是文本:python id="ly45du" s = "hello" bytes 对象是二进制数据:python id="1wh8hi" b = b"hello" 它们对于 ASCII 内容可能看起来相似,但它们是不同的类型。python id="iqtxra" print(type("hello")) # <class 'str'> print(type(b"hello")) # <class 'bytes'> Python 不会隐式混合它们:python id="8bzmko" "hello" + b"world" # TypeError 这是故意的。组合文本和字节需要编码决策。```python id="8w5t7o"
text = "hello"
data = text.encode("utf-8")
again = data.decode("utf-8")
## 14.2`str`代表 Unicode 文本
一条蟒蛇`str`是 Unicode 代码点的序列。```python id="tq2xfz"
s = "Python 🐍"
print(len(s))
len(s)计算代码点,而不是编码字节。```python id="lxgm2u"
s = "é"
print(len(s)) # 1 print(len(s.encode("utf-8"))) # 2
这种区别贯穿于 CPython 内部:```text id="c3cd4w"
str
logical Unicode text
bytes
encoded or arbitrary binary data
```## 14.3 Unicode 代码点
Unicode 代码点是由 Unicode 标准分配的整数值。
示例:
|人物 |代码点|
| ---------| ---------- |
|`A` | `U+0041` |
| `é` | `U+00E9` |
| `中` | `U+4E2D` |
| `🐍` | `U+1F40D`|
Python 通过以下方式公开了这一点`ord`和`chr`。```python id="cvt38d"
print(ord("A")) # 65
print(hex(ord("🐍"))) # 0x1f40d
print(chr(0x1f40d)) # 🐍
```从简单的通用意义上来说,字符串内部并不存储为 UTF-8 字节。 CPython 选择针对字符串内容优化的内部布局。
## 14.4 灵活的字符串表示
CPython 对 Unicode 字符串使用灵活的内部表示。
关键思想很简单:```text id="hj5qoo"
Use the smallest fixed-width storage that can represent every code point in the string.
```从概念上讲:
|字符串中的最大代码点 | 存储宽度|
| ---------------------------- | --------------------: |
|仅 ASCII | 每个字符 1 个字节 |
|最多`U+00FF`| 每个字符 1 个字节 |
|最多`U+FFFF`|每个字符 2 个字节 |
|多于`U+FFFF`|每个字符 4 个字节 |
例子:```python id="k8wqod"
"abc" # compact ASCII path
"café" # may fit in 1-byte storage
"中" # needs 2-byte storage
"🐍" # needs 4-byte storage
```此设计避免了常见 ASCII 密集文本的每个字符浪费 4 个字节,同时仍然支持所有 Unicode 代码点。
## 14.5 字符串对象元数据
CPython 字符串存储的不仅仅是字符数据。
概念领域包括:```text id="xr6949"
object header
length
hash cache
state flags
kind
compact flag
ASCII flag
ready flag
character data
optional UTF-8 cache
```确切的 C 布局是特定于版本的,但不变量比字段名称更重要。
重要元数据:
|元数据|目的|
| ------------ | -------------------------------------------------------- |
|长度 | Unicode 代码点的数量 |
|哈希缓存 |第一次计算后存储哈希 |
|亲切 |存储宽度|
| ASCII 标志 | ASCII 字符串的快速路径 |
|紧凑型旗帜|数据是否存储在对象头附近 |
| UTF-8 缓存 |供 C API 使用的缓存编码形式 |
字符串是不可变的,因此缓存的元数据是安全的。一旦计算出来,哈希值仍然有效。
## 14.6 字符串不变性
Python 字符串是不可变的。```python id="8lnw2h"
s = "hello"
s[0] = "H" # TypeError
```操作创建新字符串:```python id="q4ux6e"
s = "hello"
t = "H" + s[1:]
print(s) # hello
print(t) # Hello
```不变性给 CPython 带来了几个优点:```text id="ylorrg"
hash values can be cached
strings can be safely interned
strings can be shared across dictionaries
strings can be used as dict keys
substring operations cannot mutate originals
```代价是,如果写得不好,重复的字符串连接可能会分配很多中间字符串。
## 14.7 字符串哈希
字符串是可散列的。```python id="7pmw4s"
hash("name")
```字符串的哈希值是根据其内容计算的。由于字符串是不可变的,CPython 可以将结果缓存在字符串对象内。
这很重要,因为字符串被大量用作字典键:```text id="n69066"
module globals
object attribute names
class dictionaries
keyword argument names
JSON-like data
configuration maps
protocol field names
```如果没有缓存的字符串哈希,属性查找和字典查找将更加昂贵。
## 14.8 字符串实习
实习意味着在选定的情况下重用一个字符串对象来获得相同的字符串值。```python id="alsrhm"
a = "identifier"
b = "identifier"
print(a is b) # may be True
```CPython 实习了许多类似标识符的字符串,因为它们在属性查找和命名空间中很常见。
实习有以下用途:```text id="te68vu"
attribute names
variable names
keyword names
module names
common internal strings
```对于内部字符串,等式检查通常可以在内部路径中的哈希检查之后变成指针检查。
但用户代码不应依赖于任意字符串标识。
正确的:```python id="k4ah3b"
if a == b:
...
```错误:```python id="tgxz95"
if a is b:
...
```使用`is`仅适用于身份语义,特别是记录的单例,例如`None`。
## 14.9 编码
编码将文本转换为字节。```python id="n587av"
s = "café"
b = s.encode("utf-8")
print(b) # b'caf\xc3\xa9'
```该字符串是 Unicode 文本。 UTF-8 是一种外部字节表示形式。
常用编码:
|编码 |使用|
| -------- | ----------------------------------- |
| UTF-8 | Web、文件、API、Unix 系统 |
| UTF-16 | UTF-16一些平台API和文件格式|
| UTF-32 | UTF-32固定宽度 Unicode 存储 |
|拉丁语-1 |传统的西文字节映射 |
| ASCII | 7 位英语子集 |
CPython 不将编码视为 a 的属性`str`。字符串是解码后的文本。当跨越字节边界时使用编码。
## 14.10 解码
解码将字节转换为文本。```python id="n1sd4m"
b = b"caf\xc3\xa9"
s = b.decode("utf-8")
print(s) # café
```如果字节对于所选编码无效,则解码将失败,除非使用错误处理程序。```python id="83jkr9"
b = b"\xff"
b.decode("utf-8") # UnicodeDecodeError
b.decode("utf-8", errors="ignore")
b.decode("utf-8", errors="replace")
```错误处理程序包括:```text id="j4u46t"
strict
ignore
replace
backslashreplace
surrogateescape
surrogatepass
```编码错误是文本边界的一部分,而不是普通的字符串操作。
## 14.11 UTF-8 边界
大多数现代外部文本都是 UTF-8。
示例:```text id="ps2rks"
source files
JSON
HTTP payloads
HTML
Markdown
logs
configuration files
database text protocols
```一个实用的规则:```text id="cszoaq"
inside Python
use str
at file, network, process, and binary protocol boundaries
encode or decode explicitly
```例子:```python id="le5dmj"
from pathlib import Path
text = Path("notes.txt").read_text(encoding="utf-8")
data = text.encode("utf-8")
```这使边界保持清晰。
## 14.12 源代码编码
Python 源文件在解析之前进行解码。
除非编码声明另有说明,否则默认源编码为 UTF-8。
例子:```python id="cndzxu"
# -*- coding: utf-8 -*-
name = "café"
```分词器适用于解码后的源文本。然后字符串文字就变成了 Python`str`对象,除非它们是字节文字。```python id="wu9nv8"
s = "abc" # str
b = b"abc" # bytes
```这种区别是在解析和编译期间进行的。
## 14.13 字符串文字
Python 有多种字符串文字形式。```python id="9uk2iv"
"hello"
'hello'
"""hello"""
'''hello'''
r"\n"
f"value = {x}"
```文字前缀影响解析:
|前缀 |意义|
| ------------ | ------------------------------------------- |
|`r`|原始字符串文字 |
|`f`|格式化字符串文字 |
|`b`|字节文字 |
|`u`|历史兼容性前缀 |
|组合|`fr`, `rf`, `br`, `rb`|
原始字符串会改变解析器解释转义的方式。```python id="5il9aw"
s = r"\n"
print(s) # \n
```它仍然创建一个正常的`str`。
## 14.14 字节对象`bytes`表示不可变的二进制数据。```python id="rdc547"
b = b"hello"
```bytes 对象是 0 到 255 范围内的整数序列。```python id="a55532"
b = b"ABC"
print(b[0]) # 65
print(b[1]) # 66
```切片返回另一个字节对象:```python id="ry5nl7"
print(b[1:]) # b'BC'
```因为`bytes`是不可变的,它是可散列的。```python id="524c01"
d = {b"key": "value"}
```字节可用于:```text id="ujfy6z"
network protocols
binary files
cryptographic data
compressed data
encoded text
database wire formats
image and audio formats
```## 14.15 字节对象布局
字节对象的大小是可变的。
从概念上讲:```text id="kr49t3"
PyBytesObject
PyVarObject header
ob_size = number of bytes
hash cache
byte data
trailing NUL byte for C compatibility
```当将数据传递到需要 C 字符串的 C API 时,尾随 NUL 字节会有所帮助,但字节可能包含嵌入的 NUL 字节:```python id="z96jr9"
b = b"a\0b"
print(len(b)) # 3
```因此,字节不得被视为普通的 NUL 终止字符串,除非 API 特别允许并且内容已知是安全的。
## 14.16 字节数组对象`bytearray`是可变的二进制数据。```python id="cltuuo"
buf = bytearray(b"hello")
buf[0] = ord("H")
print(buf) # bytearray(b'Hello')
```字节数组支持就地修改:```python id="vf2c2m"
buf.append(33)
buf.extend(b" world")
```它不可散列,因为它的内容可以更改。```python id="2mamf7"
hash(bytearray(b"abc")) # TypeError
```从概念上讲,bytearray 更接近于可变字节列表,但实现为紧凑字节缓冲区,而不是 Python 整数对象引用数组。
## 14.17 字节与整数列表
比较:```python id="mm43w3"
b = b"abc"
xs = [97, 98, 99]
```bytes 对象紧凑地存储原始字节。
该列表存储对 Python 整数对象的引用。
从概念上讲:```text id="6zghb9"
bytes
[97][98][99]
list
[ptr][ptr][ptr]
| | |
v v v
int int int
```对于二进制数据,`bytes`和`bytearray`内存效率更高。
## 14.18 内存视图`memoryview`暴露另一个对象的缓冲区。```python id="lk88r7"
buf = bytearray(b"hello")
view = memoryview(buf)
view[0] = ord("H")
print(buf) # bytearray(b'Hello')
```内存视图避免复制。
在 API 之间切片或传递二进制数据时,它非常有用:```python id="fmmfbg"
data = bytearray(b"abcdef")
view = memoryview(data)[2:5]
print(view.tobytes()) # b'cde'
```该视图使导出器保持活动状态并强制执行缓冲区生存期规则。
## 14.19 缓冲区协议
buffer协议是背后的C级协议`memoryview`。
它允许对象将原始内存暴露给其他对象。
常见的缓冲区导出器:```text id="yt656l"
bytes
bytearray
array.array
memoryview
mmap
third-party arrays such as NumPy arrays
custom extension objects
```该协议描述:```text id="va15hu"
pointer to memory
length
item size
format
number of dimensions
shape
strides
readonly flag
lifetime callbacks
```这使得零拷贝二进制处理成为可能。
## 14.20 文本 I/O 与二进制 I/O
文件可以以文本模式或二进制模式打开。
文本模式将字节解码为字符串:```python id="ztfr4f"
with open("notes.txt", "r", encoding="utf-8") as f:
text = f.read()
```二进制模式返回字节:```python id="2vcxb8"
with open("notes.txt", "rb") as f:
data = f.read()
```文本模式处理编码、解码和换行符翻译。
二进制模式提供原始字节。
根据数据模型选择:```text id="ifzmcj"
human-readable text
text mode, str
protocol bytes or exact file bytes
binary mode, bytes
```## 14.21 常见边界错误
一个常见的错误是在边界处混合文本和字节。
错误:```python id="zctul0"
def send(sock, message):
sock.sendall(message) # fails if message is str
```正确的:```python id="daxwma"
def send(sock, message):
data = message.encode("utf-8")
sock.sendall(data)
```用于接收:```python id="jynmly"
data = sock.recv(4096)
text = data.decode("utf-8")
```保持转换明确,以便编码可见。
## 14.22 字符串连接
字符串是不可变的,因此连接会创建一个新字符串。```python id="fnu5rd"
s = "a"
s = s + "b"
s = s + "c"
```这样可以重复分配。
对于许多零件,更喜欢`join`:
```python id="xnzlkg"
parts = ["a", "b", "c"]
s = "".join(parts)
```对于流文本,请使用`io.StringIO`:
```python id="1icewe"
from io import StringIO
buf = StringIO()
buf.write("a")
buf.write("b")
buf.write("c")
s = buf.getvalue()
```CPython 对某些局部串联情况进行了优化,但代码不应依赖它们来获得一般性能。
## 14.23 字节构建
字节也是不可变的。
重复的字节串联也有同样的分配问题。```python id="bufv11"
data = b""
for chunk in chunks:
data += chunk
```更好的:```python id="5ojx6g"
data = b"".join(chunks)
```对于可变增量构造:```python id="bpfowr"
buf = bytearray()
for chunk in chunks:
buf.extend(chunk)
data = bytes(buf)
```对于类似文件的二进制构建:```python id="s6mtb3"
from io import BytesIO
buf = BytesIO()
buf.write(b"abc")
buf.write(b"def")
data = buf.getvalue()
```## 14.24 Unicode 规范化
不同的 Unicode 序列可能看起来相同。
例子:```python id="d3pxs7"
a = "é" # single code point U+00E9
b = "e\u0301" # e plus combining acute accent
print(a == b) # False
```它们的渲染可能类似,但它们是不同的代码点序列。
比较人类文本时标准化:```python id="7m77tv"
import unicodedata
a = unicodedata.normalize("NFC", a)
b = unicodedata.normalize("NFC", b)
print(a == b) # True
```常见的标准化形式:
|表格 |意义|
| ---- | ------------------------ | |
| NFC |规范构图 |
| NFD |规范分解|
| NFKC |相容性成分|
| NFKD |兼容性分解|
规范化是 Unicode 级别的问题,而不是 CPython 对象布局问题。但这对于正确的文本处理很重要。
## 14.25 箱子折叠
不区分大小写的比较应该经常使用`casefold`, 不是`lower`。```python id="xo53gl"
a = "Straße"
b = "strasse"
print(a.lower() == b.lower()) # often False
print(a.casefold() == b.casefold()) # True
casefold专为无外壳匹配而设计。
对于标识符、文件名、用户名和协议字段,定义精确的规范化和大小写规则。不要猜测。
14.26 字素簇
用户可见的字符可以包含多个代码点。
例子:```python id="g55aom" s = "👨👩👧👦" print(len(s))
Python`str`索引代码点,而不是字素簇。```python id="1i0lb4"
s[0]
```可能只返回可见字符的一部分。
对于 UI 文本编辑、光标移动、截断和显示宽度,代码点索引可能不够。您需要支持 Unicode 的字素分割。
## 14.27 编码错误策略
如果无法表示字符,编码可能会失败。```python id="j37zya"
"é".encode("ascii") # UnicodeEncodeError
```错误策略控制行为:```python id="9s35v1"
"é".encode("ascii", errors="ignore")
"é".encode("ascii", errors="replace")
"é".encode("ascii", errors="backslashreplace")
```对于文件和进程边界,请谨慎选择错误处理。
常见的面向 Unix 的策略是`surrogateescape`,这允许不可解码的字节往返`str`在某些文件系统和环境上下文中不会丢失数据。
## 14.28 文件系统编码
文件路径跨越文本和字节边界。
Python 通常将路径公开为`str`,但操作系统通常在编码字节序列或平台本机字符串格式上运行。
CPython 具有文件系统编码逻辑,可在 Python 字符串和平台路径表示形式之间进行转换。
实用规则:```text id="pqbj4c"
use pathlib and str paths for normal application code
use bytes paths only when you need exact low-level byte behavior
```例子:```python id="w0jnqu"
from pathlib import Path
p = Path("data") / "notes.txt"
text = p.read_text(encoding="utf-8")
```## 14.29 Unicode 的 C API 视图
C 扩展代码通常需要接受或生成字符串。
常见的操作包括:```text id="psub45"
create Unicode from UTF-8
convert Unicode to UTF-8
inspect length
read code points
parse arguments as str
```概念示例:```c id="ffcssi"
PyObject *s = PyUnicode_FromString("hello");
```和:```c id="umgbtx"
const char *p = PyUnicode_AsUTF8(obj);
```某些 API 返回的 UTF-8 指针可能指向字符串对象拥有的缓存。它的生命周期取决于拥有的对象是否存活。
扩展代码不得释放该指针。
## 14.30 C API 字节视图
字节公开连续的二进制内存。
概念示例:```c id="g34cn9"
PyObject *b = PyBytes_FromStringAndSize(data, len);
```使用权:```c id="q3e1ai"
char *p = PyBytes_AS_STRING(b);
Py_ssize_t n = PyBytes_GET_SIZE(b);
```快速宏假设该对象实际上是一个字节对象。
应在公共边界使用经过更安全检查的 API。
字节可能包含 NUL 字节,因此始终使用显式长度。
## 14.31 选择正确的类型
|需要|类型 |
| ---------------------------------- | -------------------------------------- |
|人类文本|`str`|
|编码文本 |`bytes`|
|二进制协议数据 |`bytes`|
|可变二进制缓冲区 |`bytearray`|
|零拷贝视图 |`memoryview`|
|许多文字片段|`list[str]`加`"".join`|
|许多二进制片段|`list[bytes]`加`b"".join`|
|增量文本编写器 |`io.StringIO`|
|增量二进制编写器 |`io.BytesIO`或者`bytearray`|
类型应该反映数据模型。避免使用`str`对于任意字节。避免使用`bytes`用于解码文本。
## 14.32 心智模型
使用这个模型:```text id="dluupp"
str
immutable Unicode code point sequence
optimized internal storage
cached hash
explicit encode/decode boundary
bytes
immutable byte sequence
compact binary storage
hashable
no text semantics unless decoded
bytearray
mutable byte sequence
compact binary storage
useful for incremental construction
memoryview
zero-copy view over buffer-exporting object
lifetime tied to exporter
```文本处理错误通常来自于令人困惑的代码点、字节、编码、字素簇、标准化和显示宽度。 CPython 的对象模型将文本和字节分开,因此这些决策保持明确。
## 14.33 总结`str`是 CPython 的 Unicode 文本对象。它是不可变的、可散列的,并且通过灵活的存储、缓存散列和实习进行内部优化。它表示解码的文本,而不是编码的字节。`bytes`是不可变的二进制数据。`bytearray`是可变的二进制数据。`memoryview`提供对缓冲区导出对象的零拷贝访问。
文本和二进制数据之间的界限是明确的:编码`str`生产`bytes`,并解码`bytes`生产`str`。此边界对于正确的文件 I/O、网络协议、源解码、扩展代码和 Unicode 感知应用程序至关重要。