☰
你的字符为何“变”成了数字?——Python ord() 与 chr() 的 Unicode 码点魔法与暗坑
2026/10/20 17:50:19 网站建设 项目流程

你的字符为何“变”成了数字?——Pythonord()与chr()的 Unicode 码点魔法与暗坑

在 Python 中,字符和数字之间似乎隔着一条看不见的边界:'A'是文本,65是整数。但ord()和chr()就像两扇任意门,让你能自由穿梭于字符与它的 Unicode 码点之间。ord('A')得到65,chr(65)又变回'A'。这看似简单的一对函数,却在处理多语言文本、emoji、代理码点、字节串时,布满了让人栽跟头的陷阱:ord()报TypeError说参数长度不是 1;chr()生成一个无法打印的“幽灵字符”;你以为ord('👨👩👧👦')会返回一个巨大的数字,结果它只处理了第一个码点。更隐蔽的是,很多人把ord()和encode()混为一谈,以为ord('中')会返回 UTF-8 字节值,结果拿到的是 Unicode 码点 20013,完全对不上。今天,我们就来彻底解剖ord()和chr()的精确语义,看透字符与码点之间的所有秘密。


一、问题复现:那些“长度不对”与“范围越界”的瞬间

场景 1:ord()传入空字符串或多字符字符串

ord('')# TypeError: ord() expected a character, but string of length 0 foundord('ab')# TypeError: ord() expected a character, but string of length 2 found

ord()严格只接受长度为 1的字符串。你无法用它一次性获取整个单词的码点列表,必须遍历。

场景 2:chr()传入越界数字

chr(-1)# ValueError: chr() arg not in range(0x110000)chr(0x110000)# ValueError: chr() arg not in range(0x110000)

Unicode 码点的有效范围是0到0x10FFFF(即 0 到 1,114,111)。超出这个范围,chr()直接拒绝。

场景 3:代理区码点生成“不可编码”的字符

c=chr(0xD800)# '\ud800',一个代理码点print(c)# 可能显示为空白或异常符号c.encode('utf-8')# UnicodeEncodeError: 'utf-8' codec can't encode character '\ud800' in position 0: surrogates not allowed

0xD800到0xDFFF是 UTF-16 的代理区,单独出现在字符串中是不合法的。Python 允许chr()生成它们,但一旦尝试编码为 UTF-8 或输出到终端,就会崩溃。

场景 4:误以为ord()返回编码后的字节值

ord('中')# 20013,这是 Unicode 码点'中'.encode('utf-8')# b'\xe4\xb8\xad',三个字节

ord()返回的是码点,不是 UTF-8 字节。如果你想获取字节值,应该用encode()然后取字节。

场景 5:对 emoji 使用ord()只得到第一个码点

emoji='👨👩👧👦'# 家庭 emoji,由多个码点组成print(len(emoji))# 7(或更多,取决于组合)print(ord(emoji))# TypeError: ord() expected a character, but string of length 7 foundprint(ord(emoji[0]))# 128104,只是第一个码点 👨

emoji 常常由多个 Unicode 码点组合而成(如零宽连接符、肤色修饰符)。ord()无法处理整个 emoji,只能处理单个码点。

场景 6:ord()对bytes的行为

ord(b'A')# 65,bytes 长度为 1 时,返回字节值ord(b'AB')# TypeError: ord() expected a character, but string of length 2 foundord(bytearray(b'A'))# 65

在 Python 3 中,ord()也接受长度为 1 的bytes或bytearray,返回 0-255 的字节值。这容易让人混淆:ord(b'中')会报长度错误,因为 UTF-8 的 ‘中’ 是 3 个字节。


二、底层原理:码点、字符与字节的三层世界

1. Unicode 码点

Unicode 为世界上每个字符分配一个唯一的整数,称为码点(code point)。例如:

  • 'A'→ U+0041 → 65
  • '中'→ U+4E2D → 20013
  • '😀'→ U+1F600 → 128512

码点范围从 U+0000 到 U+10FFFF,共约 111 万个。

2.ord()的语义

ord(c)->int
  • 如果c是长度为 1 的str,返回该字符的 Unicode 码点。
  • 如果c是长度为 1 的bytes或bytearray,返回该字节的整数值(0-255)。
  • 其他情况抛出TypeError。

3.chr()的语义

chr(i)->str
  • i必须是整数,范围0 <= i <= 0x10FFFF。
  • 返回一个长度为 1 的字符串,其唯一字符的 Unicode 码点为i。
  • 如果i超出范围,抛出ValueError。
  • 如果i是代理码点(0xD800-0xDFFF),返回一个包含代理字符的字符串,但该字符通常无法编码为 UTF-8。

4.ord()与chr()的互逆关系

对于有效的码点i:

ord(chr(i))==i

对于单个 Unicode 字符c:

chr(ord(c))==c

但对于bytes,ord(b'A')返回 65,chr(65)返回'A',而b'A'本身不是'A'。所以互逆关系只在str层面成立。

5. 与encode()/decode()的区别

  • ord()/chr():在码点和字符之间转换,不涉及编码。
  • encode()/decode():在字符和字节之间转换,涉及具体编码(UTF-8、GBK 等)。
'中'.encode('utf-8')# b'\xe4\xb8\xad',3 个字节ord('中')# 20013,1 个码点

6. 码点、码元与字素簇

  • 码点:Unicode 的基本单位。
  • 码元:编码后的最小单元(如 UTF-8 的字节,UTF-16 的 16 位单元)。
  • 字素簇:人类感知的一个字符,可能由多个码点组成(如 emoji 家庭、带音标的字母)。

ord()只处理单个码点,无法感知字素簇。


三、常见陷阱与错误模式

陷阱 1:ord()参数长度不为 1

ord('hello')# TypeError

应遍历字符串:

[ord(c)forcin'hello']# [104, 101, 108, 108, 111]

陷阱 2:chr()传入负数或超范围整数

chr(-5)# ValueErrorchr(2**20)# ValueError

始终检查范围:

if0<=i<=0x10FFFF:c=chr(i)else:raiseValueError("无效码点")

陷阱 3:代理码点导致编码失败

c=chr(0xD800)c.encode('utf-8')# UnicodeEncodeError

避免生成代理码点,除非你在处理 UTF-16 代理对。

陷阱 4:误用ord()获取字节值

ord('中')# 20013,不是 UTF-8 字节

获取 UTF-8 字节值应使用:

list('中'.encode('utf-8'))# [228, 184, 173]

陷阱 5:对组合字符使用ord()

s='e\u0301'# 'é' 的分解形式:e + 重音符号len(s)# 2ord(s)# TypeError

应使用unicodedata.normalize规范化,或遍历码点。

陷阱 6:chr()生成的字符终端无法显示

c=chr(0x1F600)# 😀print(c)# 可能显示为方块或乱码,取决于终端字体

这是显示问题,不是 Python 错误。字符本身是有效的。

陷阱 7:混淆ord()与int()的转换

int('65')# 65,字符串转整数ord('A')# 65,字符转码点

两者完全不同。

陷阱 8:ord()对空字节串

ord(b'')# TypeError

陷阱 9:chr()的参数必须是整数

chr('65')# TypeErrorchr(65.0)# TypeError

陷阱 10:Unicode 码点与 UTF-16 代理对

# 对于超出 BMP 的字符(如 U+1F600)c=chr(0x1F600)len(c)# 1(Python 3 中,一个码点就是一个字符)# 但在 UTF-16 中,它由两个代理码元表示

Python 3 的str是码点序列,不是 UTF-16 码元序列。len()返回码点数。


四、正确解决方案:码点操作的黄金法则

1. 遍历字符串获取所有码点

s='Hello 中文'codes=[ord(c)forcins]print(codes)# [72, 101, 108, 108, 111, 32, 20013, 25991]

2. 从码点列表还原字符串

codes=[72,101,108,108,111]s=''.join(chr(c)forcincodes)print(s)# 'Hello'

3. 检查码点有效性

defsafe_chr(i):ifnotisinstance(i,int):raiseTypeError("码点必须是整数")ifnot(0<=i<=0x10FFFF):raiseValueError(f"码点{i}超出 Unicode 范围")if0xD800<=i<=0xDFFF:raiseValueError(f"码点{i}是代理码点,不能单独使用")returnchr(i)

4. 获取字符的十六进制码点表示

c='中'print(hex(ord(c)))# 0x4e2dprint(f"U+{ord(c):04X}")# U+4E2D

5. 使用unicodedata获取字符信息

importunicodedata c='中'print(unicodedata.name(c))# 'CJK UNIFIED IDEOGRAPH-4E2D'print(unicodedata.category(c))# 'Lo'(字母,其他)

6. 处理组合字符:规范化

importunicodedata s='e\u0301'# e + 组合重音normalized=unicodedata.normalize('NFC',s)# 'é'print(len(normalized))# 1print(ord(normalized))# 233

7. 生成字符范围

# 生成 A-Zletters=[chr(i)foriinrange(ord('A'),ord('Z')+1)]print(letters)# ['A', 'B', ..., 'Z']# 生成所有 ASCII 可打印字符printable=[chr(i)foriinrange(32,127)]

8. 与encode()/decode()明确分工

c='中'code_point=ord(c)# 20013utf8_bytes=c.encode('utf-8')# b'\xe4\xb8\xad'utf8_values=list(utf8_bytes)# [228, 184, 173]

9. 处理 emoji 和复杂字素簇

importunicodedata emoji='👨👩👧👦'# 获取所有码点code_points=[ord(c)forcinemoji]print([hex(cp)forcpincode_points])# 使用第三方库 grapheme 或 regex 处理字素簇# pip install graphemeimportgraphemeprint(grapheme.length(emoji))# 1 个字素

10. 安全转换用户输入

defchar_to_code(s):iflen(s)!=1:raiseValueError("请输入单个字符")returnord(s)defcode_to_char(i):try:returnchr(i)exceptValueErrorase:raiseValueError(f"无效码点:{i}")frome

五、调试与验证技巧

  1. 打印repr():查看字符的真实表示。
    print(repr('中'))# '中'print(repr(chr(0xD800)))# '\ud800'
  2. 使用hex()和U+格式:方便对照 Unicode 表。
  3. 检查len():确认是单个码点还是多个。
  4. 使用unicodedata.name():确认字符身份,避免混淆相似字符。
  5. 编码测试:对chr()生成的字符尝试.encode('utf-8'),捕获代理码点。
  6. 在 CI 中测试边界码点:0、0x10FFFF、代理区。
  7. 使用sys.maxunicode:Python 3 中通常为0x10FFFF。
  8. 对比ord和encode结果:强化码点与字节的区别。

六、最佳实践总结

  • ord()只接受长度为 1 的str(或bytes/bytearray),返回码点或字节值。
  • chr()接受 0 到 0x10FFFF 的整数,返回对应字符。
  • 码点不是字节,不要混淆ord()和encode()。
  • 避免生成代理码点(0xD800-0xDFFF)。
  • 处理多字符字符串时,遍历每个字符调用ord()。
  • 处理 emoji 时,注意它们可能由多个码点组成,使用字素簇库。
  • 使用unicodedata进行规范化和字符信息查询。
  • 始终检查chr()参数的范围和类型。
  • 在文档和代码中明确区分码点、字节和字素簇。
  • 为涉及 Unicode 的代码编写边界测试。

七、结语

ord()和chr()是 Python 中连接字符与数字的两座小桥,它们精确、快速,却绝不宽容:长度不对、范围越界、代理码点、多码点 emoji,每一个都会让你在运行时撞上异常。理解 Unicode 码点、编码字节和字素簇的三层模型,你就能在字符的世界里自由穿梭,而不会把ord('中')误当成 UTF-8 字节。从今天起,当你需要字符的数字身份时,请想起ord();当你需要从数字重塑字符时,请想起chr()。只要记住它们只处理“单个码点”,并在边界处保持警惕,你就能在 Unicode 的浩瀚海洋中精准导航,让每一个字符都找到它正确的数字坐标。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询