PEP 3138 – Python 3000 中的字符串表示
- 作者:
- Atsuo Ishimoto <ishimoto at gembook.org>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2008年5月5日
- Python 版本:
- 3.0
- 历史记录:
- 2008年5月5日,2008年6月5日
摘要
本 PEP 提出了一种用于 Python 3000 的新的字符串表示形式。在 Python 3000 之前的 Python 版本中,内置函数 repr() 将任意对象转换为可打印的 ASCII 字符串,用于调试和日志记录。对于 Python 3000,应该将基于 Unicode 标准的更广泛的字符范围视为“可打印的”。
动机
当前的 repr() 使用以下算法将 8 位字符串转换为 ASCII。
- 将 CR、LF、TAB 和 ‘\’ 转换为 ‘\r’、‘\n’、‘\t’、‘\\’。
- 将其他不可打印字符 (0x00-0x1f, 0x7f) 和非 ASCII 字符 (>= 0x80) 转换为 ‘\xXX’。
- 反斜杠转义引号字符(撇号,')并在开头和结尾添加引号字符。
对于 Unicode 字符串,会执行以下其他转换。
- 将没有尾随字符的前导代理对字符 (0xd800-0xdbff,但后面不跟着 0xdc00-0xdfff) 转换为 ‘\uXXXX’。
- 将 16 位字符 (>= 0x100) 转换为 ‘\uXXXX’。
- 将 21 位字符 (>= 0x10000) 和代理对字符转换为 ‘\U00xxxxxx’。
此算法将任何字符串转换为可打印的 ASCII,并且 repr() 被用作一种方便且安全的方式来打印字符串以进行调试或日志记录。尽管所有非 ASCII 字符都已转义,但在大多数字符串的字符都是 ASCII 时,这无关紧要。但是对于其他语言,例如日语,其中字符串中的大多数字符都不是 ASCII,这非常不方便。
我们可以使用 print(aJapaneseString)
获取可读的字符串,但对于打印来自集合(如列表或元组)的字符串,我们没有类似的解决方法。print(listOfJapaneseStrings)
使用 repr() 构建要打印的字符串,因此生成的字符串始终是十六进制转义的。或者当 open(japaneseFilename)
抛出异常时,错误消息类似于 IOError: [Errno 2] No such file or directory: '\u65e5\u672c\u8a9e'
,这没有帮助。
Python 3000 具有许多针对非拉丁语用户的不错功能,例如非 ASCII 标识符,因此如果 Python 也能在可打印输出方面以类似的方式发展,那将非常有帮助。
一些用户可能会担心,如果他们打印图像等二进制数据,此类输出会弄乱他们的控制台。但这在实践中不太可能发生,因为在 Python 3000 中,字节和字符串是不同的类型,因此将图像打印到控制台不会弄乱它。
此问题曾由 Hye-Shik Chang [1] 讨论过,但被拒绝了。
规范
- 向 Python C API 添加一个新函数
int Py_UNICODE_ISPRINTABLE (Py_UNICODE ch)
。如果 repr() 应该转义 Unicode 字符ch
,则此函数返回 0;否则返回 1。在 Unicode 字符数据库中将要转义的字符定义为- Cc(其他,控制)
- Cf(其他,格式)
- Cs(其他,代理)
- Co(其他,专用使用)
- Cn(其他,未分配)
- Zl(分隔符,行),指的是行分隔符(’\u2028’)。
- Zp(分隔符,段落),指的是段落分隔符(’\u2029’)。
- Zs(分隔符,空格),除了 ASCII 空格(’\x20’)。应转义此类别中的字符以避免歧义。
- 构建 repr() 字符串的算法应更改为
- 将 CR、LF、TAB 和 ‘\’ 转换为 ‘\r’、‘\n’、‘\t’、‘\\’。
- 将不可打印的 ASCII 字符 (0x00-0x1f, 0x7f) 转换为 ‘\xXX’。
- 将没有尾随字符的前导代理对字符 (0xd800-0xdbff,但后面不跟着 0xdc00-0xdfff) 转换为 ‘\uXXXX’。
- 将不可打印的字符 (Py_UNICODE_ISPRINTABLE() 返回 0) 转换为 ‘\xXX’、‘\uXXXX’ 或 ‘\U00xxxxxx’。
- 反斜杠转义引号字符(撇号,0x27)并在开头和结尾添加引号字符。
- 默认情况下将 sys.stderr 的 Unicode 错误处理程序设置为 ‘backslashreplace’。
- 向 Python C API 添加一个新函数
PyObject *PyObject_ASCII (PyObject *o)
。此函数使用 PyObject_Repr() 将任何 Python 对象转换为字符串,然后对所有非 ASCII 字符进行十六进制转义。PyObject_ASCII()
生成与 Python 2 中的PyObject_Repr()
相同的字符串。 - 添加一个新的内置函数
ascii()
。此函数使用 repr() 将任何 Python 对象转换为字符串,然后对所有非 ASCII 字符进行十六进制转义。ascii()
生成与 Python 2 中的repr()
相同的字符串。 - 添加
'%a'
字符串格式运算符。'%a'
使用 repr() 将任何 Python 对象转换为字符串,然后对所有非 ASCII 字符进行十六进制转义。'%a'
格式运算符生成与 Python 2 中的'%r'
相同的字符串。此外,将'!a'
转换标志添加到string.format()
方法中,并将'%A'
运算符添加到 PyUnicode_FromFormat() 中。它们将任何对象转换为 ASCII 字符串,如'%a'
字符串格式运算符。 - 向字符串类型添加
isprintable()
方法。str.isprintable()
如果 repr() 将转义字符串中的任何字符,则返回 False;否则返回 True。isprintable()
方法在内部调用Py_UNICODE_ISPRINTABLE()
函数。
基本原理
Python 3000 中的 repr() 应该基于 Unicode,而不是 ASCII,就像 Python 3000 字符串一样。此外,转换不应受区域设置的影响,因为区域设置不一定与输出设备的区域设置相同。例如,守护进程在 ASCII 设置中被调用,但将其 UTF-8 写入其日志文件的情况很常见。此外,Web 应用程序可能希望根据 HTML 页面的编码以更易读的形式报告错误信息。
用户控制台不支持的字符可以在打印时由 Unicode 编码器的错误处理程序进行十六进制转义。如果输出文件的错误处理程序是 ‘backslashreplace’,则此类字符会在不引发 UnicodeEncodeError 的情况下进行十六进制转义。例如,如果默认编码是 ASCII,则 print('Hello ¢')
将打印 ‘Hello \xa2’。如果编码是 ISO-8859-1,则会打印 ‘Hello ¢’。
sys.stdout 的默认错误处理程序是 ‘strict’。读取输出的其他应用程序可能无法理解十六进制转义的字符,因此在写入时应捕获不支持的字符。如果必须转义不支持的字符,则应显式更改错误处理程序。与 sys.stdout 不同,sys.stderr 默认不会引发 UnicodeEncodingError,因为默认错误处理程序是 ‘backslashreplace’。因此,将包含非 ASCII 字符的错误消息打印到 sys.stderr 不会引发异常。此外,解释器在不引发异常的情况下打印有关未捕获异常的信息(异常对象、回溯)。
替代方案
为了帮助在不更改 repr() 的情况下调试非拉丁语语言,提出了其他建议。
- 提供一个用于打印列表或字典的工具。
用于调试的字符串不仅包含在列表或字典中,还包含在许多其他类型的对象中。文件对象包含用 Unicode 表示的文件名,异常对象包含用 Unicode 表示的消息,等等。当使用 repr() 时,这些字符串应该以可读的形式打印。实现一个用于打印所有可能的对象类型的工具不太可能。
- 使用 sys.displayhook 和 sys.excepthook。
对于交互式会话,我们可以编写挂钩以将十六进制转义的字符恢复为原始字符。但是,只有在打印在交互式 Python 会话中输入的表达式的结果时才会调用这些挂钩,并且不适用于
print()
函数、非交互式会话或logging.debug("%r", ...)
等。 - 子类化 sys.stdout 和 sys.stderr。
很难实现一个用于恢复十六进制转义字符的子类,因为在它成为字符串时,没有留下足够的信息来在所有情况下正确地撤消转义。例如,
print("\\"+"u0041")
应该打印为 ‘\u0041’,而不是 ‘A’。但是没有机会区分文件对象。 - 使 unicode_repr() 使用的编码可调整,并使现有的 repr() 成为默认值。
使用可调整的 repr(),使用 repr() 的结果是不可预测的,并且会使编写涉及 repr() 的正确代码变得不可能。如果当前的 repr() 是默认值,则旧约定将保持不变,用户可能会期望 repr() 的结果为 ASCII 字符串。当使用自定义 repr() 函数时,第三方应用程序或库可能会感到困惑。
向后兼容性
更改 repr() 可能会破坏一些现有代码,尤其是测试代码。Python 的五个回归测试在此修改后失败。如果您需要与 Python 2 一样没有非 ASCII 字符的 repr() 字符串,可以使用以下函数。
def repr_ascii(obj):
return str(repr(obj).encode("ASCII", "backslashreplace"), "ASCII")
对于日志记录或调试,以下代码可能会引发 UnicodeEncodeError。
log = open("logfile", "w")
log.write(repr(data)) # UnicodeEncodeError will be raised
# if data contains unsupported characters.
为了避免引发异常,您可以显式指定错误处理程序。
log = open("logfile", "w", errors="backslashreplace")
log.write(repr(data)) # Unsupported characters will be escaped.
对于使用基于 Unicode 的编码的控制台,例如 en_US.utf8 或 de_DE.utf8,反斜杠替换技巧不起作用,并且所有可打印字符都不会转义。这会导致在西方、希腊语和斯拉夫语语言中类似地绘制字符的问题。这些语言使用类似(但不同)的字母表(源自共同的祖先),并且包含看起来相似但具有不同字符代码的字母。例如,很难区分拉丁字母 ‘a’、‘e’ 和 ‘o’ 与西里尔字母 ‘а’、‘е’ 和 ‘о’。(当然,视觉表示很大程度上取决于使用的字体,但通常这些字母几乎无法区分。)为了避免此问题,用户可以调整终端编码以获得适合其环境的结果。
被拒绝的提案
- 向内置 print() 函数添加 encoding 和 errors 参数,其默认值为 sys.getfilesystemencoding() 和 ‘backslashreplace’。
实现起来很复杂,总的来说,这被认为不是一个好主意。[2]
- 使用字符名称转义字符,而不是十六进制字符代码。例如,
repr('\u03b1')
可以转换为"\N{GREEK SMALL LETTER ALPHA}"
。与十六进制转义相比,使用字符名称可能会非常冗长。例如,
repr("\ufbf9")
会转换为"\N{ARABIC LIGATURE UIGHUR KIRGHIZ YEH WITH HAMZA ABOVE WITH ALEF MAKSURA ISOLATED FORM}"
。 - sys.stdout 的默认错误处理程序应为“backslashreplace”。
写入 stdout 的内容可能会被另一个程序使用,该程序可能会错误地解释 \ 转义字符。对于交互式会话,可以将“backslashreplace”错误处理程序设置为默认值,但这可能会导致“在交互模式下有效,但在重定向到文件时无效”之类的混淆。
实现
作者在 http://bugs.python.org/issue2630 中编写了一个补丁;该补丁已在 2008 年 11 月 6 日的修订版本 64138 中提交到 Python 3.0 分支。
参考文献
版权
本文档已进入公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-3138.rst
上次修改时间:2023-09-09 17:39:29 GMT