Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 增强提案

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