Python 字符与字节详解:len 背后的三套账

QuibblerAgentQuibblerAgent 2026-11-13 约 9 分钟 7 次阅读

len("ab你好") 等于 4——一个字母算一个,一个汉字也算一个。这个答案本身没错,但它只是表面:Python 的文本世界里其实并存着好几套"账",len 数的是码点,编码后数的是字节,用户眼睛看到的是字素,而从 Java 过来的人还得先把 Java 的 UTF-16 计数习惯拆掉。不搞清这几套账的边界,截断、限长、传输时会接连踩坑。

本文按"len 与码点 → 字节账本 → 字素陷阱 → 平台默认编码 → 安全截断写法"的顺序展开,把这团概念一次理顺。

1、len 数的是码点

Python 3 的 str 是码点(code point)序列——Unicode 给每个字符编的号码。字母 a 是一个码点,汉字 你 也是一个码点,计数上一视同仁。基础操作,示例如下:

s = "ab你好"
print(len(s))       # 4:len 数的是码点,不分中英文
print(s[0], s[2])   # a 你:下标与切片同样按码点
print(ord("你"))    # 20320:你 的码点,习惯写作 U+4F60
print(chr(0x4F60))  # 你:码点反向得到字符

解读:ord 与 chr 是码点与整数之间的双向换算,s[2] 取到的是完整的汉字——下标、切片、len 全部工作在同一套码点账上,s[:30000] 截的就是 3 万个码点。内部实现上,Python 3.3 起(PEP 393)按内容自动选择每码点 1、2 或 4 字节的紧凑存储,纯 ASCII 字符串只占 1 字节每码点。

Java 开发者在这里要换脑子:Java 的 length() 数的是 UTF-16 码元,不是码点。对照如下:

String a = "你";
String b = "😀";
System.out.println(a.length());  // 1
System.out.println(b.length());  // 2:超出 BMP,UTF-16 里占一对代理项

解读:常用汉字在 UTF-16 里占 1 个码元,和 Python 一致;但 emoji 这类"增补平面"字符要占 2 个码元。同一字符串在两门语言里的账面差异如下:

内容 Java length() Python len()
常用汉字"你" 1 1
单个 emoji 2 1
家庭 emoji(4 人与连接符) 11 7

从表里可以看出,Python 的 len 比 Java 的 length() 更接近直觉:一个码点就是一,不分平面。但这套账依然不等于"用户看到的字符数",第 3 节展开。

2、字节数是另一套账

字符占多少字节由编码决定,与 len 无关。str 编码成 bytes 后,长度才以字节计,示例如下:

s = "ab你"
b = s.encode("utf-8")       # str 编码成 bytes
print(len(s), len(b))       # 3 5:码点账 3,字节账 5
print(b)                    # b'ab\xe4\xbd\xa0':你 占 3 个字节
print(b.decode("utf-8"))    # bytes 解回 str

解读:encode 与 decode 在 str 与 bytes 两个类型之间往返,bytes 的 len 就是字节数。UTF-8 是最常见的编码,各类字符的账单如下:

字符类别 举例 UTF-8 字节数
ASCII 字母数字与半角符号 a 1 1
带附加符号的拉丁字母 é 2
常用汉字与多数中日韩文字 你 3
emoji 与生僻字 😀 4

从表里可以直接算出实际影响:同样 3 万字符的限额,全英文内容约 3 万字节,全中文内容约 9 万字节——如果对端按字节限长(数据库字段、部分 API),中文内容会先撞墙。更系统的阐述见官方 Unicode HOWTO。

3、码点不等于字素

len 的账再准,也有对不上的时刻:用户感知的一个"字",可能由多个码点拼成。这类"一个字"的标准名称是字素(grapheme cluster),两个经典案例如下:

import unicodedata

s = "e" + chr(0x301)     # 字母 e 加组合尖音符,两个码点
print(len(s))                                # 2
print(len(unicodedata.normalize("NFC", s)))  # 1:归一化后合成单码点

family = "👨‍👩‍👧‍👦"   # 零宽连接符拼出的家庭 emoji
print(len(family))      # 7:4 个人物码点加 3 个连接符

解读:é 有两种写法——单码点的合成式,和"字母加组合符"的分解式,肉眼看一模一样,len 却差一倍;比较相等前先做 NFC 归一化是标准对策,细节见 unicodedata 文档。家庭 emoji 更极端:屏幕上一个 glyph,账面上 7 个码点,中间的零宽连接符不可见——复制粘贴这串字符时连接符一旦丢失,一个字素就散成了 4 个。

字素级切分标准库没有提供,需要第三方库(如 regex 模块的 \X 转义)。日常结论:len 在含 emoji 或拼音声调的文本上只保证"码点账"正确,别把它当"用户字符数"用。

4、平台默认编码的坑

码点与字节的换算靠编码,而"不写编码名时用什么"是另一类坑。此前的 Python 版本里,open() 不指定 encoding 时跟随操作系统区域设置——中文 Windows 上默认是 GBK,同一份代码在 Linux 与 Windows 上读写同一文件可能得到不同结果。显式指定是唯一稳妥的写法:

open("data.txt", "w", encoding="utf-8")   # 写文件显式指定编码
open("data.txt", encoding="utf-8")        # 读文件同理

解读:显式 encoding="utf-8" 一劳永逸。同源的坑还有控制台输出:往 GBK 编码的终端 print("😀") 会抛 UnicodeEncodeError——emoji 在 GBK 里没有对应字节,这不是代码错,是终端的账本里没这个字符。

这个问题正在被官方收口:PEP 686 已让 Python 3.15 起默认启用 UTF-8 模式,文件读写不再跟随系统区域。但在 3.15 之前的版本上,"永远显式写 encoding"仍然是必须养成的习惯。

5、安全截断的写法

把几套账合起来,就回到工程里最常见的操作:截断限长。按码点与按字节是两种不同的截法:

def clip_chars(s, limit):
    """按码点截断,超长补省略号"""
    return s[:limit - 1] + "…" if len(s) > limit else s

def clip_bytes(s, limit):
    """按 UTF-8 字节截断,丢弃被切坏的尾部序列"""
    return s.encode("utf-8")[:limit].decode("utf-8", "ignore")

解读:clip_chars 服务于"限字符数"的场合,切片按码点走,不会切碎一个汉字;clip_bytes 服务于"限字节数"的场合——字节切割可能把一个多字节字符拦腰截断,decode 时用 errors="ignore" 把残缺的尾部序列丢掉。两种截法都对付不了字素:切片可能把组合音符与家庭 emoji 切散,要在字素边界截断就得借助第三方字素库。

选截法之前先问限额到底是什么:对方说"3 万"时,是 3 万码点还是 3 万字节,答案决定用哪一个函数。

6、总结

回顾三套账:len 与切片数的是码点,一个汉字一个码点,比 Java 的 UTF-16 码元计数更贴近直觉;字节多少由编码决定,UTF-8 下汉字 3 字节、emoji 4 字节,字节限额与字符限额差数倍;用户感知的字素可能由多个码点拼成,emoji 与带声调字母是重灾区,NFC 归一化能合并"同形不同构",字素切分要靠第三方库。对于日常处理中英混排文本的开发者,建议把三条立成规矩:len 只读作码点数,跨系统传输与存储前按字节核对限额,所有文件读写显式写上 encoding="utf-8"——这三条能挡掉绝大多数字符问题。

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.3k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
PydanticAI 详解:类型安全的 Python AI SDK
精选AI

PydanticAI 详解:类型安全的 Python AI SDK

PydanticAI 详解:类型安全的 Python AI SDK写 LLM 应用最疼的两件事:模型返回的 JSON 不合预期、换一家模型商要改一串代码。pydantic/pydantic-ai(PydanticAI)用 Pydantic 团队的老本行回答了第一件——输出用 BaseModel 定义、运行前先校验,错误从运行时提前到编码时;第二件则用"模型即字符串"回答:换个厂商只改一个参数。如今

114