一切皆“字节”
首先要记住一个核心概念:计算机在底层存储和传输的所有数据,本质上都是二进制字节(bytes),编码(Encoding)和解码(Decoding)就是在这两种形式之间进行转换的桥梁。

- 字符串:是人类可读的文本,在 Python 2 和 Python 3 中,它的内部表示是不同的。
- 字节:是计算机能处理的原始数据,比如一个文件、一段网络报文。
Python 2 的编码世界
Python 2 的设计存在一些历史遗留问题,导致处理编码时非常容易出错。
两种字符串类型
Python 2 中有两种字符串类型:
str:字节串,它直接存储原始的字节,不关心这些字节代表什么字符。'hello'在 Python 2 中是一个str类型,它存储的是每个字符的 ASCII 码对应的字节。unicode:Unicode 字符串,它存储的是抽象的字符,与具体的编码方式无关。u'你好'就是一个unicode类型。
# Python 2 # str 类型,字节串 s = 'hello' print type(s) # <type 'str'> print repr(s) # 'hello' (在 ASCII 环境下,每个字符占一个字节) # unicode 类型 u = u'你好' print type(u) # <type 'unicode'> print repr(u) # u'\u4f60\u597d' (存储的是字符的 Unicode 码点)
编码与解码
要在 str 和 unicode 之间转换,就需要使用 编码 和 解码。
- 编码 (Encode):将
unicode字符串转换成str字节串。unicode_string.encode(encoding)
- 解码 (Decode):将
str字节串转换成unicode字符串。byte_string.decode(encoding)
# Python 2
# 假设源代码文件是 UTF-8 编写的
# -*- coding: utf-8 -*-
# 1. unicode -> str (编码)
u_str = u'你好'
# 将 unicode 字符串用 'utf-8' 编码成字节串
s_bytes = u_str.encode('utf-8')
print type(s_bytes) # <type 'str'>
print repr(s_bytes) # '\xe4\xbd\xa0\xe5\xa5\xbd' (这是 '你好' 的 UTF-8 编码后的字节)
# 2. str -> unicode (解码)
# 将字节串用 'utf-8' 解码成 unicode 字符串
u_str_back = s_bytes.decode('utf-8')
print type(u_str_back) # <type 'unicode'>
print repr(u_str_back) # u'\u4f60\u597d'
Python 2 的“隐式解码”陷阱
这是 Python 2 最坑的地方,当你混合使用 str 和 unicode 时,Python 会尝试隐式地将 str 解码成 unicode。

# Python 2
# 假设 s_str 是一个 GBK 编码的 '你好'
s_str = '\xc4\xe3\xba\xc3' # '你好' 的 GBK 编码
u_str = u'hello'
# Python 尝试将 s_str 解码成 unicode,然后和 u_str 连接
# 它使用什么编码来解码?答案是系统的默认编码 (通常是 ASCII)
# 在非 ASCII 系统上,这会直接抛出 UnicodeDecodeError
# try:
# result = u_str + s_str
# except UnicodeDecodeError as e:
# print e # 'ascii' codec can't decode byte 0xc4 in position 0: ordinal not in range(128)
# 正确的做法是显式解码
s_str_unicode = s_str.decode('gbk')
result = u_str + s_str_unicode
print result # hello你好
这个隐式转换是 Python 2 中大部分乱码问题的根源。
文件 I/O
Python 2 打开文件时,默认是二进制模式,读出来的是 str(字节串),写进去也需要是 str。
# Python 2
# 写入文件
# 需要显式地将 unicode 编码成 str 再写入
content = u'这是中文内容'
with open('test.txt', 'w') as f:
f.write(content.encode('utf-8'))
# 读取文件
# 读出来的是 str,需要显式解码
with open('test.txt', 'r') as f:
content_bytes = f.read()
content_unicode = content_bytes.decode('utf-8')
print content_unicode # 这是中文内容
Python 3 的编码世界
Python 3 对编码问题进行了彻底的改革,设计上更加清晰和一致。
两种字符串类型
Python 3 也有两种字符串类型,但含义和 Python 2 完全不同:

str:Unicode 字符串,这是默认的字符串类型,用于表示文本。'hello'在 Python 3 中是一个str类型,但它内部存储的是 Unicode 字符。bytes:字节串,专门用来存储原始的字节数据,类似于 Python 2 的str。
# Python 3 # str 类型,Unicode 字符串 s = 'hello' print(type(s)) # <class 'str'> print(repr(s)) # 'hello' # bytes 类型,字节串 b = b'hello' print(type(b)) # <class 'bytes'> print(repr(b)) # b'hello' (每个字符都是一个字节)
编码与解码
Python 3 的转换规则非常明确:
- 编码 (Encode):将
str(Unicode) 转换成bytes。string.encode(encoding)
- 解码 (Decode):将
bytes转换成str(Unicode)。bytes_object.decode(encoding)
# Python 3
# 1. str -> bytes (编码)
s_str = '你好'
# 将 unicode 字符串用 'utf-8' 编码成字节串
b_bytes = s_str.encode('utf-8')
print(type(b_bytes)) # <class 'bytes'>
print(repr(b_bytes)) # b'\xe4\xbd\xa0\xe5\xa5\xbd'
# 2. bytes -> str (解码)
# 将字节串用 'utf-8' 解码成 unicode 字符串
s_str_back = b_bytes.decode('utf-8')
print(type(s_str_back)) # <class 'str'>
print(repr(s_str_back)) # '你好'
关键区别:在 Python 3 中,str 和 bytes 是完全不能直接拼接的,这从源头上杜绝了 Python 2 的隐式解码陷阱。
# Python 3 s_str = 'hello' b_bytes = b'world' # 下面这行代码会直接报 TypeError,非常清晰 # result = s_str + b_bytes # TypeError: can only concatenate str (not "bytes") to str
文件 I/O
Python 3 在文件 I/O 上做了巨大改进,引入了 io 模块,并明确区分了文本模式和二进制模式。
-
文本模式 (
'r','w'):- 读取:从文件中读取字节,然后自动解码成
str(Unicode) 返回。 - 写入:将
str(Unicode) 自动编码成字节后写入文件。 - 你可以通过
encoding参数指定编码(强烈推荐!默认是系统编码,可能出问题)。
- 读取:从文件中读取字节,然后自动解码成
-
二进制模式 (
'rb','wb'):- 读取:从文件中读取原始字节,直接返回
bytes对象,不做任何解码。 - 写入:将
bytes对象直接写入文件,不做任何编码。
- 读取:从文件中读取原始字节,直接返回
# Python 3
# 写入文件 (文本模式)
# 自动编码
content = '这是中文内容'
with open('test_py3.txt', 'w', encoding='utf-8') as f:
f.write(content)
# 读取文件 (文本模式)
# 自动解码
with open('test_py3.txt', 'r', encoding='utf-8') as f:
content_str = f.read()
print(type(content_str)) # <class 'str'>
print(content_str) # 这是中文内容
# 写入文件 (二进制模式)
# 需要手动编码
content_bytes = content.encode('utf-8')
with open('test_py3.bin', 'wb') as f:
f.write(content_bytes)
# 读取文件 (二进制模式)
# 需要手动解码
with open('test_py3.bin', 'rb') as f:
content_bytes_read = f.read()
print(type(content_bytes_read)) # <class 'bytes'>
content_str_back = content_bytes_read.decode('utf-8')
print(content_str_back) # 这是中文内容
总结与最佳实践
| 特性 | Python 2 | Python 3 |
|---|---|---|
| 默认字符串 | str (字节串) |
str (Unicode 字符串) |
| 字节串 | str |
bytes |
| Unicode 字符串 | unicode |
str |
| 核心转换 | unicode.encode() -> strstr.decode() -> unicode |
str.encode() -> bytesbytes.decode() -> str |
| 混合类型操作 | 危险! 会尝试隐式解码,容易出错。 | 安全! 直接 TypeError,强制你进行显式转换。 |
| 文件读写 | 默认二进制模式,需手动编解码。 | 文本模式:自动编解码,需指定 encoding。二进制模式 ( 'rb'/'wb'):直接读写 bytes。 |
最佳实践
-
明确区分文本和二进制:
- 处理文本内容(如日志、配置、网页内容)时,始终使用
str(Python 3) 或unicode(Python 2)。 - 处理原始数据(如图片、音频、网络包、文件二进制内容)时,始终使用
bytes(Python 3) 或str(Python 2)。
- 处理文本内容(如日志、配置、网页内容)时,始终使用
-
尽早解码,尽可能晚编码:
- 在程序的入口(如读取文件、网络请求),尽早将输入的字节流解码成
str(Unicode)。 - 在程序的出口(如写入文件、发送网络请求),尽可能晚地将
str(Unicode) 编码成字节流。
- 在程序的入口(如读取文件、网络请求),尽早将输入的字节流解码成
-
统一内部编码:
- 让你的整个应用程序在内部都使用
str(Python 3) 或unicode(Python 2) 来处理所有文本数据,只在与外界(文件、网络、数据库)交互时,才进行编解码。
- 让你的整个应用程序在内部都使用
-
指定编码,不要依赖默认值:
- 在打开文件、网络请求等所有地方,显式地指定编码,通常是
'utf-8'。open(file, 'r', encoding='utf-8')。
- 在打开文件、网络请求等所有地方,显式地指定编码,通常是
-
拥抱 Python 3:
Python 3 的设计从根本上解决了 Python 2 的编码混乱问题,如果你是新项目,请毫不犹豫地使用 Python 3,如果你维护 Python 2 代码,应尽快规划迁移到 Python 3。
