Python 中 ord() 和 chr() 负责在单个字符与 Unicode 码点之间互转,看似简单,实际涉及码点、编码字节和字素簇三层模型。原文强调的核心事实是:ord() 只接受长度为 1 的 str,或长度为 1 的 bytes/bytearray;chr() 只接受 0 到 0x10FFFF 的整数。理解这两个边界,比记住函数名更重要。
【先厘清码点、字节和字素簇】
Unicode 码点是字符的唯一整数编号,例如 'A' 是 65,'中' 是 20013,'😀' 是 128512。ord(c) 返回的是码点,不是 UTF-8 编码后的字节值。'中'.encode('utf-8') 得到三个字节,而 ord('中') 仍是 20013。Python 3 的 str 是码点序列,len() 返回码点数;UTF-8 的码元是字节,UTF-16 的码元是 16 位单元。emoji 和带音标字母还可能是多个码点组成的字素簇,ord() 无法直接处理整个字素簇。
【典型错误与原因】
第一类错误是 ord() 参数长度不为 1:- ord('')
- # TypeError: ord() expected a character, but string of length 0 found
- ord('ab')
- # TypeError: ord() expected a character, but string of length 2 found
- ord(b'')
- # TypeError
复制代码 要获取整个字符串的码点,必须遍历:- codes = [ord(c) for c in 'hello']
- print(codes)
- # [104, 101, 108, 108, 111]
复制代码 第二类错误是 chr() 参数越界或类型不对:- chr(-1)
- # ValueError: chr() arg not in range(0x110000)
- chr(0x110000)
- # ValueError: chr() arg not in range(0x110000)
- chr('65')
- # TypeError
- chr(65.0)
- # TypeError
复制代码 Unicode 码点有效范围是 0 到 0x10FFFF。chr() 只接受整数,不接受字符串或浮点数。
第三类错误是代理码点导致编码失败。0xD800 到 0xDFFF 是 UTF-16 代理区,Python 允许 chr() 生成代理字符,但单独放入字符串后通常不能编码为 UTF-8:- c = chr(0xD800)
- print(repr(c))
- c.encode('utf-8')
- # UnicodeEncodeError: surrogates not allowed
复制代码 除非在专门处理 UTF-16 代理对,否则应避免生成代理码点。
第四类错误是把 ord() 当成 encode()。ord('中') 返回 20013,是码点;要拿 UTF-8 字节值应使用 encode():- print(ord('中'))
- # 20013
- print(list('中'.encode('utf-8')))
- # [228, 184, 173]
复制代码 ord() 与 encode() 的职责不同:前者做字符到码点,后者做字符到编码字节。
第五类错误是对 emoji 或组合字符直接调用 ord()。家庭 emoji 由多个码点组成,ord(emoji) 会因字符串长度不为 1 报 TypeError;ord(emoji[0]) 只能取第一个码点。组合字符如 e 加重音符号也是两个码点,需要遍历或先做 Unicode 规范化:- import unicodedata
- s = 'e' + chr(0x0301)
- print(len(s))
- # 2
- normalized = unicodedata.normalize('NFC', s)
- print(len(normalized))
- print(ord(normalized))
- # 233
复制代码 另外,ord() 对长度为 1 的 bytes 或 bytearray 会返回 0 到 255 的字节值,例如 ord(b'A') 返回 65,ord(bytearray(b'A')) 也返回 65;但 ord(b'AB') 会报长度错误。这个行为容易与 str 的码点语义混淆,使用时应明确输入类型。
【可落地的安全写法】
对 chr() 做类型、范围和代理区检查,可以避免大部分运行时异常:- def safe_chr(i):
- if not isinstance(i, int):
- raise TypeError('码点必须是整数')
- if not (0 <= i <= 0x10FFFF):
- raise ValueError(f'码点 {i} 超出 Unicode 范围')
- if 0xD800 <= i <= 0xDFFF:
- raise ValueError(f'码点 {i} 是代理码点,不能单独使用')
- return chr(i)
复制代码 从码点列表还原字符串时,可用 join 配合 chr():- codes = [72, 101, 108, 108, 111]
- s = ''.join(chr(c) for c in codes)
- print(s)
- # Hello
复制代码 生成字符范围也是常见用法,例如生成 A 到 Z:- letters = [chr(i) for i in range(ord('A'), ord('Z') + 1)]
- print(letters)
复制代码 获取字符的十六进制码点表示,便于对照 Unicode 表:- c = '中'
- print(hex(ord(c)))
- # 0x4e2d
- print(f'U+{ord(c):04X}')
- # U+4E2D
复制代码 需要查询字符身份时,可用 unicodedata:- import unicodedata
- c = '中'
- print(unicodedata.name(c))
- print(unicodedata.category(c))
复制代码 处理用户输入时,应明确要求单个字符,并校验长度:- def char_to_code(s):
- if len(s) != 1:
- raise ValueError('请输入单个字符')
- return ord(s)
- def code_to_char(i):
- try:
- return chr(i)
- except ValueError as e:
- raise ValueError(f'无效码点: {i}') from e
复制代码
【调试与边界测试】
调试 Unicode 问题时,优先打印 repr(),确认字符真实表示;用 hex() 和 U+ 格式对照码点;用 len() 判断是单个码点还是多个码点;用 unicodedata.name() 确认字符身份。对 chr() 生成的字符尝试 encode('utf-8'),可以暴露代理码点问题。终端显示为方块或乱码不一定是 Python 错误,也可能是字体不支持,字符本身仍可能有效。测试中应覆盖 0、0x10FFFF、代理区以及多码点 emoji 等边界。
【最佳实践】
ord() 只处理单个码点,多字符字符串要遍历;chr() 参数必须是 0 到 0x10FFFF 的整数,并避开代理区;码点不是字节,不要用 ord() 代替 encode();处理 emoji 和组合字符时,注意字素簇与规范化;在输入校验、文档和测试中明确区分码点、字节和字素簇。只要牢记 ord() 与 chr() 只围绕单个码点工作,并在边界处主动校验,就能减少 TypeError、ValueError 和 UnicodeEncodeError 带来的排查成本。 |