Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 538 – 强制传统的 C 区域设置使用基于 UTF-8 的区域设置

作者:
Alyssa Coghlan <ncoghlan at gmail.com>
BDFL 委托
INADA Naoki
状态:
最终版
类型:
标准跟踪
创建日期:
2016 年 12 月 28 日
Python 版本:
3.7
发布历史:
2017 年 1 月 3 日,2017 年 1 月 7 日,2017 年 3 月 5 日,2017 年 5 月 9 日
决议:
Python-Dev 消息

目录

摘要

在 *nix 系统上,Python 3 面临的一个持续挑战是,需要在默认情况下使用已配置的区域设置编码,以与同一进程或子进程中的其他区域设置感知组件保持一致,但标准 C 区域设置(如 POSIX:2001 中定义)通常意味着默认文本编码为 ASCII,这对于在多语言世界中开发网络服务和客户端应用程序来说是完全不够的。

PEP 540 提议更改 CPython 对传统 C 区域设置的处理方式,以便 CPython 在此类环境中假定使用 UTF-8,而不是坚持使用明显存在问题的 ASCII 作为与操作系统接口通信的适当编码的假设。对于网络编码互操作性比本地编码互操作性更重要的场景,这是一个很好的方法。

然而,这样做的代价是 CPython 的编码假设将与其同一进程中的其他区域设置感知组件以及共享相同环境的子进程中运行的组件的编码假设产生分歧。

这可能会导致与某些扩展模块(例如 GNU readline 的命令行历史编辑)以及与在子进程中运行的组件(例如旧的 Python 运行时)的互操作性问题。

它还需要对 CPython 自身的工作原理进行非平凡的更改,而不是主要依赖于 Python 3.7 之前的 Python 版本支持的现有配置设置。

因此,本 PEP 提议,独立于 PEP 540 中提议的 UTF-8 模式,CPython 实现处理默认 C 区域设置的方式应更改为大致等同于以下现有配置设置(自 Python 3.1 起支持)

LC_CTYPE=C.UTF-8
PYTHONIOENCODING=utf-8:surrogateescape

用于强制的目标区域设置将根据实际可用的区域设置从预定义列表中在运行时选择。

重新解释的区域设置将写回环境中,以便它们在同一进程和子进程中的其他组件可见,但更改后的 PYTHONIOENCODING 默认值将是隐式的,以避免与不提供 surrogateescape 错误处理程序的 Python 2 子进程产生兼容性问题。

新的传统区域设置强制行为可以通过设置 LC_ALL(这仍可能导致 Unicode 兼容性警告)或将新的 PYTHONCOERCECLOCALE 环境变量设置为 0 来禁用。

通过此更改,任何 *nix 平台如果其标准配置中提供至少 C.UTF-8C.utf8UTF-8 区域设置之一,则只有在明确配置了除默认 C 区域设置之外的合适区域设置(例如 en_AU.UTF-8zh_CN.gb18030)时,才被视为 CPython 3.7+ 部署的完全支持平台。如果除了本 PEP 之外还接受了 PEP 540,那么在使用提议的 PYTHONUTF8 模式时,纯 Python 模块也将受到支持,但对扩展模块中完全 Unicode 兼容性的期望将继续仅限于本 PEP 涵盖的平台。

由于它只反映了默认设置的更改,而不是一个根本性的新功能,因此目标受众比上游 CPython 开发团队更窄的再分发商(如 Linux 发行版)也可以选择通过应用必要的更改作为下游补丁,将其用于 Python 3.6.x 系列的区域设置强制行为。

实现说明

尝试实现最初接受的 PEP 表明,默认情况下发出区域设置强制和兼容性警告的提议根本不切实际(有太多情况下以前正常工作的代码因为警告而不是因为受影响代码中潜在的区域设置处理缺陷而失败)。

因此,删除了 PY_WARN_ON_C_LOCALE 配置标志,并将其替换为运行时 PYTHONCOERCECLOCALE=warn 环境变量设置,允许开发人员和系统集成商选择接收区域设置强制和兼容性警告,而无需默认发出它们。

PEP 本身中的输出示例也已更新,以删除警告并使其更易于阅读。

背景

CPython 解释器启动时,可能需要以与整个系统的区域设置一致的方式,从 char * 格式转换为 wchar_t * 格式,或从其中一种格式转换为 PyUnicodeObject *。它通过依靠操作系统进行转换,然后确保 sys.getfilesystemencoding() 报告的文本编码名称与此早期引导过程中使用的编码匹配来处理这些情况。

在 Windows 上,这些转换中默认使用的 mbcs 格式的限制被证明问题足够严重,以至于 PEP 528PEP 529 被实现,以绕过操作系统提供的二进制数据处理接口,强制使用 UTF-8。

在 Mac OS X、iOS 和 Android 上,许多组件,包括 CPython,已经假定使用 UTF-8 作为系统编码,无论区域设置如何。然而,并非所有组件都如此,这种差异在某些情况下可能会导致问题(例如,使用 GNU readline 模块时 [16])。

在非 Apple 和非 Android 的 *nix 系统上,这些操作是使用 glibc 中的 C 区域设置系统处理的,它具有以下特性 [4]

  • 默认情况下,所有进程都以 C 区域设置启动,该区域设置使用 ASCII 进行这些转换。这几乎不是任何进行多语言文本处理的人实际想要的(包括 CPython 和 C/C++ GUI 框架)。
  • 调用 setlocale(LC_ALL, "") 根据当前进程环境中配置的区域设置类别重新配置活动区域设置
  • 如果当前环境请求的区域设置未知,或者未配置特定区域设置,则默认的 C 区域设置将保持活动状态

CPython 依赖的 API 所涵盖的特定区域设置类别是 LC_CTYPE,它适用于“字符的分类和转换,以及多字节和宽字符”[5]。因此,CPython 包含以下关键的 setlocale 调用

  • 在主 python 二进制文件中,CPython 调用 setlocale(LC_ALL, "") 以根据进程环境配置整个 C 区域设置子系统。它在调用共享 CPython 库之前执行此操作
  • Py_Initialize 中,CPython 调用 setlocale(LC_CTYPE, ""),这样该类别配置的区域设置始终与环境中设置的区域设置匹配。它无条件地执行此操作,并且在 Py_Finalize恢复进程状态更改

(此区域设置处理摘要省略了与声明为区域设置设置一部分的文本编码的具体使用位置和时间相关的几个技术细节——有关进一步讨论,请参阅 PEP 540,因为这些特定细节在将 CPython 与声明的 C 区域设置解耦时比在用基于 UTF-8 的区域设置覆盖区域设置时更重要)

这些调用通常足以提供合理的行为,但它们在以下情况下仍可能失败

  • SSH 环境转发意味着 SSH 客户端有时可能将客户端区域设置转发到未安装该区域设置的服务器。这导致 CPython 在基于 ASCII 的默认 C 区域设置中运行
  • 某些进程环境(例如 Linux 容器)可能根本没有配置任何显式区域设置。与未知区域设置一样,这导致 CPython 在基于 ASCII 的默认 C 区域设置中运行
  • 在 Android 上,不是根据环境变量配置区域设置,而是将空区域设置 "" 视为专门请求 "C" 区域设置

对于当前发布的 CPython 版本,解决此问题最简单的方法是在启动应用程序时显式设置一个更合理的区域设置。例如

LC_CTYPE=C.UTF-8 python3 ...

C.UTF-8 区域设置是一个完整的区域设置定义,它将 LC_CTYPE 类别用于 UTF-8,并对所有其他类别(包括 LC_COLLATE)使用与 C 区域设置相同的设置。它由许多 Linux 发行版(包括 Debian、Ubuntu、Fedora、Alpine 和 Android)提供,作为基于 ASCII 的 C 区域设置的替代方案。其他一些平台(例如 HP-UX)以 C.utf8 名称提供等效的区域设置定义。

Mac OS X 和其他 *BSD 系统采取了不同的方法:它们不提供 C.UTF-8 区域设置,而是提供一个只定义 LC_CTYPE 类别的部分 UTF-8 区域设置。在此类系统上,首选的环境区域设置调整是设置 LC_CTYPE=UTF-8,而不是设置 LC_ALLLANG[17]

在 Docker 容器和类似技术的特定情况下,可以在容器镜像定义中直接指定适当的区域设置。

另一个常见的失败案例是开发人员指定 LANG=C 以便以英文查看原本已翻译的用户界面消息,而不是范围更窄的 LC_MESSAGES=CLANGUAGE=en

与其他 PEP 的关系

本 PEP 与 PEP 540 具有共同的问题陈述(改进 Python 3 在默认 C 区域设置中的行为),但在提议的解决方案上存在显著差异

  • PEP 540 提议在这种情况下完全将 CPython 的默认文本编码与 C 区域设置系统解耦,从而导致 CPython 与在同一进程和子进程中运行的其他区域设置感知组件之间出现文本处理不一致。这种方法旨在使 CPython 的行为更不像区域设置感知应用程序,而更像 Go、Node.js (V8) 和 Rust 等区域设置无关的语言运行时
  • 本 PEP 提议用一个最近定义的区域设置覆盖传统 C 区域设置,该区域设置使用 UTF-8 作为其默认文本编码。这意味着文本编码覆盖不仅适用于 CPython,还适用于加载到当前进程中的任何区域设置感知扩展模块,以及在从父进程继承环境的子进程中调用的区域设置感知应用程序。这种方法旨在保留 CPython 传统上对与其他区域设置感知组件集成的强大支持,同时积极推动 C.UTF-8 区域设置在更广泛的 C/C++ 生态系统中作为传统 C 区域设置的 Unicode 感知替代方案的采用和标准化

在审查了这两个 PEP 后,很明显它们在技术层面并不冲突,PEP 540 中的提议在没有合适的区域设置可用的情况下提供了更优越的选项,并且为“区域设置编码”概念没有意义的平台(例如,运行 MicroPython 而不是 CPython 参考解释器的嵌入式系统)提供了更好的参考行为。

同时,本 PEP 提供了与其他区域设置感知组件的改进兼容性,并且是一种更容易被下游再分发商回溯到 Python 3.6 的方法。

因此,本 PEP 进行了修订,将 PEP 540 视为一种补充解决方案,它在没有标准基于 UTF-8 的区域设置可用时提供了改进的行为,并且将默认设置中的更改扩展到当前无法独立配置的 API(例如 open() 的默认编码和错误处理程序)。

PEP 540 的可用性也意味着 LC_CTYPE=en_US.UTF-8 传统回退从 UTF-8 区域设置列表中删除,这些区域设置被尝试作为强制目标,期望 CPython 在这种情况下将完全依赖于提议的 PYTHONUTF8 模式。

动机

虽然 Docker、Kubernetes 和 OpenShift 等 Linux 容器技术以其在 Web 服务开发中的应用而闻名,但相关的容器格式和执行模型也正在被用于 Linux 命令行应用程序开发。Gnome Flatpak [7] 和 Ubuntu Snappy [8] 等技术进一步旨在将这些相同的技术引入 Linux GUI 应用程序开发。

在这种情况下使用 Python 3 进行应用程序开发时,经常会看到与文本编码相关的错误,类似于以下内容

$ docker run --rm fedora:25 python3 -c 'print("ℙƴ☂ℌøἤ")'
Unable to decode the command from the command line:
UnicodeEncodeError: 'utf-8' codec can't encode character '\udce2' in position 7: surrogates not allowed
$ docker run --rm ncoghlan/debian-python python3 -c 'print("ℙƴ☂ℌøἤ")'
Unable to decode the command from the command line:
UnicodeEncodeError: 'utf-8' codec can't encode character '\udce2' in position 7: surrogates not allowed

即使相同的命令在本地运行时可能正常工作

$ python3 -c 'print("ℙƴ☂ℌøἤ")'
ℙƴ☂ℌøἤ

问题根源可以通过在三种环境中运行 locale 命令来查看

$ locale | grep -E 'LC_ALL|LC_CTYPE|LANG'
LANG=en_AU.UTF-8
LC_CTYPE="en_AU.UTF-8"
LC_ALL=
$ docker run --rm fedora:25 locale | grep -E 'LC_ALL|LC_CTYPE|LANG'
LANG=
LC_CTYPE="POSIX"
LC_ALL=
$ docker run --rm ncoghlan/debian-python locale | grep -E 'LC_ALL|LC_CTYPE|LANG'
LANG=
LANGUAGE=
LC_CTYPE="POSIX"
LC_ALL=

在这个特定的例子中,我们可以看到主机系统的区域设置被设置为“en_AU.UTF-8”,因此CPython使用UTF-8作为默认文本编码。相比之下,Fedora和Debian的基础Docker镜像没有设置任何特定的区域设置,因此它们默认使用POSIX区域设置,它是基于ASCII的默认C区域设置的别名。

在基于 Fedora 和 Debian 的容器中,让 Python 3(无论具体版本如何)表现合理的最简单方法是在两个发行版都提供的 C.UTF-8 区域设置中运行它

$ docker run --rm -e LC_CTYPE=C.UTF-8 fedora:25 python3 -c 'print("ℙƴ☂ℌøἤ")'
ℙƴ☂ℌøἤ
$ docker run --rm -e LC_CTYPE=C.UTF-8 ncoghlan/debian-python python3 -c 'print("ℙƴ☂ℌøἤ")'
ℙƴ☂ℌøἤ

$ docker run --rm -e LC_CTYPE=C.UTF-8 fedora:25 locale | grep -E 'LC_ALL|LC_CTYPE|LANG'
LANG=
LC_CTYPE=C.UTF-8
LC_ALL=
$ docker run --rm -e LC_CTYPE=C.UTF-8 ncoghlan/debian-python locale | grep -E 'LC_ALL|LC_CTYPE|LANG'
LANG=
LANGUAGE=
LC_CTYPE=C.UTF-8
LC_ALL=

Docker, Inc. 提供的基于 Alpine Linux 的 Python 镜像默认已经使用 C.UTF-8 区域设置

$ docker run --rm python:3 python3 -c 'print("ℙƴ☂ℌøἤ")'
ℙƴ☂ℌøἤ
$ docker run --rm python:3 locale | grep -E 'LC_ALL|LC_CTYPE|LANG'
LANG=C.UTF-8
LANGUAGE=
LC_CTYPE="C.UTF-8"
LC_ALL=

同样,对于自定义容器镜像(即在基础发行版镜像之上添加额外内容的镜像),可以在镜像定义中设置更合适的区域设置,这样一切都默认正常工作。然而,如果 CPython 能够自动处理这个问题,而不是依赖于再分发商或最终用户通过系统配置更改来处理,那么它将提供更好、更一致的用户体验。

尽管 glibc 开发人员正在努力使 C.UTF-8 区域设置普遍适用于基于 glibc 的应用程序(如 CPython)[6],但这不幸地对那些发布了没有该功能的旧版本 glibc 的平台没有帮助,并且这些平台也没有像 Debian 和 Fedora 那样提供 C.UTF-8(或等效的)作为磁盘上的区域设置。这些平台不属于本 PEP 的范围——有关在这些环境中改进 CPython 默认行为的可能选项的进一步讨论,请参阅 PEP 540

设计原则

上述动机引出了所提出解决方案的以下核心设计原则

  • 如果明确配置了除默认 C 区域设置之外的区域设置,我们将继续尊重它
  • 尽可能地,所做的任何更改都将使用现有配置选项
  • 无论区域设置是在环境中显式设置还是作为区域设置强制目标隐式设置,Python 在潜在强制目标区域设置中的运行时行为都应该相同
  • 对于 Python 3.7,如果我们更改区域设置而没有明确的配置选项,我们将在 stderr 上发出警告,而不是默默地更改进程配置。这将提醒应用程序和系统集成商注意此更改,即使他们没有密切关注 PEP 过程或 Python 发布公告。然而,为了最大程度地减少给最终用户带来新问题的可能性,我们将使用警告系统来执行此操作,因此即使使用 -Werror 也不会将其变成运行时异常。(注意:这些警告最终默认被禁用。有关更多详细信息,请参阅上面的实现说明)
  • 对于 Python 3.7,任何更改的默认值都将提供某种形式的显式“关闭”开关,可在构建时、运行时或两者兼有。

最大限度地减少对当前已正确配置为使用 GB-18030 或其他部分 ASCII 兼容的通用编码的系统造成的负面影响,这导致了以下设计原则

  • 如果一个基于 UTF-8 的 Linux 容器运行在一个明确配置为使用非 UTF-8 编码的主机上,并且试图与该主机交换本地编码数据而不是交换明确的 UTF-8 编码数据,CPython 将努力正确往返于仅在常见 ASCII 兼容代码点处连接或拆分的主机提供的数据,但否则可能会发出无意义的结果。

最大限度地减少对已正确配置为使用 LC_TIMELC_MONETARYLC_NUMERIC 等显式区域设置类别,但在其他情况下仍在传统 C 区域设置中运行的系统和程序造成的负面影响,这带来了以下设计原则

  • 不要进行任何会改变 LC_CTYPE 以外的区域设置类别现有设置的环境更改(最值得注意的是:不要设置 LC_ALLLANG

最后,为保持与编排用例中运行任意子进程的兼容性,引入了以下设计原则

  • 不要进行任何可能与仍受支持的 CPython 版本(包括 CPython 2.7)不兼容的 Python 特定环境更改

规范

为了更好地处理 CPython 在其他情况下最终会尝试在 C 区域设置中操作的情况,本 PEP 提议 CPython 在作为独立命令行应用程序运行时,自动尝试将传统的 C 区域设置强制转换为 LC_CTYPE 类别的基于 UTF-8 的区域设置。

它进一步提议,如果在语言运行时本身初始化时,LC_CTYPE 类别仍生效传统 C 区域设置,并且未设置禁用区域设置强制的显式环境标志,则在 stderr 上发出警告,以警告系统和应用程序集成商他们正在以不受支持的配置运行 CPython。

除了这些一般性更改之外,还提出了一些额外的 Android 特定更改,以处理该平台上 setlocale 行为的差异。

独立 Python 解释器二进制文件中的传统 C 区域设置强制

当作为独立应用程序运行时,CPython 有机会在进程中执行任何区域设置相关操作之前重新配置 C 区域设置。

这意味着它不仅可以更改 CPython 运行时的区域设置,还可以更改当前进程中运行的任何其他区域设置感知组件(例如作为扩展模块的一部分)以及从当前进程继承其环境的子进程中的区域设置。

调用 setlocale(LC_ALL, "") 以初始化当前进程中的区域设置后,主解释器二进制文件将更新以包含以下调用

const char *ctype_loc = setlocale(LC_CTYPE, NULL);

这个神秘的调用是 C 语言提供的用于查询当前区域设置而不改变它的 API。鉴于该查询,可以使用 strcmp 准确地检查 C 区域设置

ctype_loc != NULL && strcmp(ctype_loc, "C") == 0 # true only in the C locale

当未设置特定区域设置,或者名义区域设置设置为 C 区域设置的别名(例如 POSIX)时,此调用也返回 "C"

有了这些信息,CPython 就可以尝试将区域设置强制转换为使用 UTF-8 而不是 ASCII 作为默认编码的区域设置。

将尝试以下三种区域设置

  • C.UTF-8 (至少在 Debian、Ubuntu、Alpine 和 Fedora 25+ 中可用,预计在未来的 glibc 版本中默认可用)
  • C.utf8 (至少在 HP-UX 中可用)
  • UTF-8 (至少在某些 *BSD 变体中可用,包括 Mac OS X)

强制将通过将 LC_CTYPE 环境变量设置为候选区域设置名称来实现,以便未来的 setlocale() 调用将看到它,并且其他寻找这些设置的组件(例如 GUI 开发框架和 Python 自己的 locale 模块)也将看到它。

为了实现更好的跨平台二进制可移植性,并自动适应未来区域设置可用性的变化,这些检查将在除 Windows 以外的所有平台上的运行时实现,而不是尝试在编译时确定要尝试哪些区域设置。

当此区域设置强制激活时,以下警告将打印到 stderr,警告中包含成功配置的区域设置

Python detected LC_CTYPE=C: LC_CTYPE coerced to C.UTF-8 (set another
locale or PYTHONCOERCECLOCALE=0 to disable this locale coercion behaviour).

(注意:此警告最终默认被禁用。有关更多详细信息,请参阅上面的实现说明)

只要当前平台提供至少一个基于 UTF-8 的候选环境,这种区域设置强制将意味着标准 Python 二进制文件区域设置感知扩展应该在当前已知的三个主要故障案例(缺少区域设置设置、通过 LANGLC_CTYPE 进行未知区域设置的 SSH 转发,以及开发人员明确请求 LANG=C)中再次“正常工作”。

唯一可能仍然发生故障的情况是,当 stderr 专门检查没有输出时,这可以通过配置非 C 区域设置,或者使用“stderr 没有输出”之外的机制来检查子进程错误(例如检查进程返回代码)来解决。

如果没有任何候选区域设置成功配置,或者在当前进程环境中定义了 LC_ALL 区域设置覆盖,则初始化将继续在 C 区域设置中进行,并且将像任何其他应用程序一样发出下一节中描述的 Unicode 兼容性警告。

如果明确设置了 PYTHONCOERCECLOCALE=0,初始化将继续在 C 区域设置中进行,并且 Unicode 兼容性警告将自动抑制。

解释器将在启动时始终检查 PYTHONCOERCECLOCALE 环境变量(即使在 -E-I 开关下运行),因为区域设置强制检查必然在任何命令行参数处理之前进行。为了一致性,用于确定是否抑制区域设置兼容性警告的运行时检查也将独立于这些设置。

运行时初始化期间的传统 C 区域设置警告

在调用 Py_Initialize 时,当前进程中可能已发生了任意依赖于区域设置的操作。这意味着在调用它时,可靠地切换到不同的区域设置已经太晚了——这样做即使在独立的 Python 解释器二进制文件的上下文中也会引入解码文本的不一致性。

因此,当调用 Py_Initialize 并且 CPython 检测到已配置的区域设置仍然是默认的 C 区域设置且未设置 PYTHONCOERCECLOCALE=0 时,将发出以下警告

Python runtime initialized with LC_CTYPE=C (a locale with default ASCII
encoding), which may cause Unicode compatibility problems. Using C.UTF-8,
C.utf8, or UTF-8 (if available) as alternative Unicode-compatible
locales is recommended.

(注意:此警告最终默认被禁用。有关更多详细信息,请参阅上面的实现说明)

在这种情况下,不会对区域设置进行任何实际更改。

相反,警告会告知系统和应用程序集成商,他们正在以我们不期望正常工作的配置运行 Python 3。

提供建议的第二句话最终可能会根据操作系统进行条件编译(例如,在 *BSD 系统上推荐 LC_CTYPE=UTF-8),但初始实现将只使用上面所示的通用消息。

新的构建时配置选项

虽然上述两种行为都将默认启用,但它们也将具有新的相关配置选项和预处理器定义,以方便希望覆盖这些默认设置的再分发商。

区域设置强制行为将由标志 --with[out]-c-locale-coercion 控制,该标志将设置 PY_COERCE_C_LOCALE 预处理器定义。

区域设置警告行为将由标志 --with[out]-c-locale-warning 控制,该标志将设置 PY_WARN_ON_C_LOCALE 预处理器定义。

(注意:此编译时警告选项最终被运行时 PYTHONCOERCECLOCALE=warn 选项取代。有关更多详细信息,请参阅上面的实现说明)

在不使用 autotools 构建系统(即 Windows)的平台上,这些预处理器变量将始终未定义。

标准流上的默认错误处理更改

自 Python 3.5 起,当 CPython 检测到当前区域设置为 C 并且未使用 PYTHONIOENCODING 环境变量或 Py_setStandardStreamEncoding API 设置特定错误处理程序时,它会默认为标准流(sys.stdinsys.stdout)使用 surrogateescape。对于其他区域设置,标准流的默认错误处理程序是 strict

为了在不引入区域设置强制和显式配置区域设置之间任何行为差异的情况下保留此行为,强制目标区域设置(C.UTF-8C.utf8UTF-8)将被添加到使用 surrogateescape 作为其标准流默认错误处理程序的区域设置列表中。

未提议更改 sys.stderr 的默认错误处理程序:它将继续是 backslashreplace

Android 上的区域设置更改

独立于本 PEP 中的其他更改,Android 系统上的 CPython 将更新为在当前调用 setlocale(LC_ALL, "") 的地方调用 setlocale(LC_ALL, "C.UTF-8"),并在当前调用 setlocale(LC_CTYPE, "") 的地方调用 setlocale(LC_CTYPE, "C.UTF-8")

引入此 Android 特有行为是由于以下 Android 特有细节

  • 在 Android 上,将 "" 传递给 setlocale 等同于传递 "C"
  • C.UTF-8 区域设置始终可用

平台支持更改

PEP 11 中将添加一个新的“传统 C 区域设置”部分,其中指出

  • 自 CPython 3.7 起,*nix 平台预计至少提供 C.UTF-8(完整区域设置)、C.utf8(完整区域设置)或 UTF-8(仅 LC_CTYPE 区域设置)作为传统 C 区域设置的替代方案。仅在传统 C 区域设置中发生且无法在适当配置的非 ASCII 区域设置中重现的 Unicode 相关集成问题将以“不会修复”结束。

基本原理

改进 C 区域设置的处理

很长一段时间以来,C 区域设置的默认编码 ASCII 对于现代网络服务的开发来说完全是错误的选择。像 Rust 和 Go 这样的新语言完全摒弃了这一默认设置,而是将其作为部署要求,即系统应配置为使用 UTF-8 作为操作系统接口的文本编码。同样,Node.js 默认假定 UTF-8(继承自 V8 JavaScript 引擎),并且需要自定义构建设置来指示它应使用系统区域设置进行区域设置感知操作。JVM 和 .NET CLR 都使用 UTF-16-LE 作为它们在应用程序和应用程序运行时(即 JVM/CLR,而不是主机操作系统)之间传递文本的主要编码。

CPython 面临的挑战在于,除了用于网络服务开发之外,它还被广泛用作大型应用程序中的嵌入式脚本语言和桌面应用程序开发语言,在这种情况下,与共享同一进程的其他区域设置感知组件以及用户的桌面区域设置保持一致比与现代网络服务开发的新兴惯例保持一致更重要。

本 PEP 的核心前提是,对于所有这些用例,默认“C”区域设置隐含的 ASCII 假设是错误的选择,并且以下假设是有效的

  • 在桌面应用程序用例中,进程区域设置将已经配置适当,如果不是,那么这是一个操作系统或嵌入式应用程序级别的问题,需要报告给操作系统提供商或应用程序开发人员并由他们解决
  • 在网络服务开发用例中(尤其是那些基于 Linux 容器的),进程区域设置可能根本没有配置,如果不是,那么期望是组件将强制执行自己的默认编码,就像 Rust、Go 和 Node.js 所做的那样,而不是像 CPython 当前那样信任传统的 C 默认编码 ASCII

在标准 IO 流上默认为“surrogateescape”错误处理

通过强制区域设置脱离传统的 C 默认值及其对 ASCII 作为首选文本编码的假设,本 PEP 还禁用了 Python 3.5 中引入的对标准 IO 流上“surrogateescape”错误处理程序的隐式使用([15]),以及在 PEP 540 提议的 UTF-8 模式下操作时自动使用 surrogateescape

本 PEP 不再引入另一个配置选项来调整该行为,而是提议将 stdinstderr 错误处理的“surrogateescape”默认值也应用于三个潜在的强制目标区域设置。

此行为的目的是尝试确保即使 Python 3 应用程序错误地假定操作系统提供的文本已编码为 UTF-8,也能透明地传递。

特别是,GB 18030 [12] 是一个处理所有 Unicode 代码点的中国国家文本编码标准,它与 ASCII 和 UTF-8 在形式上不兼容,但仍然经常容忍作为代理转义数据进行处理——GB 18030 以不兼容方式重用 ASCII 字节值的地方很可能在 UTF-8 中无效,因此将被转义,并且对于在相关 ASCII 代码点处拆分或搜索的字符串处理操作来说是不透明的。不涉及在特定 ASCII 或 Unicode 代码点处拆分或搜索的操作几乎肯定会正确工作。

同样,Shift-JIS [13] 和 ISO-2022-JP [14] 在日本仍然广泛使用,并且与 ASCII 和 UTF-8 不兼容,但会容忍不涉及在特定 ASCII 或 Unicode 代码点处拆分或搜索的文本处理操作。

例如,考虑两个文件,一个用 UTF-8 编码(en_AU.UTF-8 的默认编码),另一个用 GB-18030 编码(zh_CN.gb18030 的默认编码)

$ python3 -c 'open("utf8.txt", "wb").write("ℙƴ☂ℌøἤ\n".encode("utf-8"))'
$ python3 -c 'open("gb18030.txt", "wb").write("ℙƴ☂ℌøἤ\n".encode("gb18030"))'

在磁盘上,我们可以看到这是两个非常不同的文件

$ python3 -c 'print("UTF-8:  ", open("utf8.txt", "rb").read().strip()); \
              print("GB18030:", open("gb18030.txt", "rb").read().strip())'
UTF-8:   b'\xe2\x84\x99\xc6\xb4\xe2\x98\x82\xe2\x84\x8c\xc3\xb8\xe1\xbc\xa4\n'
GB18030: b'\x816\xbd6\x810\x9d0\x817\xa29\x816\xbc4\x810\x8b3\x816\x8d6\n'

只要在打印前解码,它们都可以正确渲染到终端

$ python3 -c 'print("UTF-8:  ", open("utf8.txt", "r", encoding="utf-8").read().strip()); \
              print("GB18030:", open("gb18030.txt", "r", encoding="gb18030").read().strip())'
UTF-8:   ℙƴ☂ℌøἤ
GB18030: ℙƴ☂ℌøἤ

相比之下,如果我们像 cat 和类似的 C/C++ 工具那样直接传递原始字节

$ LANG=en_AU.UTF-8 cat utf8.txt gb18030.txt
ℙƴ☂ℌøἤ
�6�6�0�0�7�9�6�4�0�3�6�6

即使设置了特定的中文区域设置,也无助于正确渲染 GB-18030 编码文件

$ LANG=zh_CN.gb18030 cat utf8.txt gb18030.txt
ℙƴ☂ℌøἤ
�6�6�0�0�7�9�6�4�0�3�6�6

问题在于,无论名义区域设置如何,终端编码设置仍然是 UTF-8。可以使用 iconv 工具模拟 GB18030 终端

$ cat utf8.txt gb18030.txt | iconv -f GB18030 -t UTF-8
鈩櫰粹槀鈩屆羔激
ℙƴ☂ℌøἤ

这反转了问题,使得 GB18030 文件正确渲染,但 UTF-8 文件已转换为不相关的汉字字符,而不是预期的“Python”作为非 ASCII 字符的渲染。

在模拟的 GB18030 终端编码下,Python 中假定 UTF-8 会导致两个文件都显示不正确

$ python3 -c 'print("UTF-8:  ", open("utf8.txt", "r", encoding="utf-8").read().strip()); \
              print("GB18030:", open("gb18030.txt", "r", encoding="gb18030").read().strip())' \
  | iconv -f GB18030 -t UTF-8
UTF-8:   鈩櫰粹槀鈩屆羔激
GB18030: 鈩櫰粹槀鈩屆羔激

然而,正确设置区域设置意味着模拟的 GB18030 终端现在会按最初的意图显示这两个文件

$ LANG=zh_CN.gb18030 \
  python3 -c 'print("UTF-8:  ", open("utf8.txt", "r", encoding="utf-8").read().strip()); \
              print("GB18030:", open("gb18030.txt", "r", encoding="gb18030").read().strip())' \
  | iconv -f GB18030 -t UTF-8
UTF-8:   ℙƴ☂ℌøἤ
GB18030: ℙƴ☂ℌøἤ

保留 surrogateescape 作为默认 IO 编码的理由是,它将在 C 区域设置中保留以下有用的行为

$ cat gb18030.txt \
  | LANG=C python3 -c "import sys; print(sys.stdin.read())" \
  | iconv -f GB18030 -t UTF-8
ℙƴ☂ℌøἤ

而不是回到显式配置基于 UTF-8 的区域设置时当前出现的异常

$ cat gb18030.txt \
  | python3 -c "import sys; print(sys.stdin.read())" \
  | iconv -f GB18030 -t UTF-8
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/usr/lib64/python3.5/codecs.py", line 321, in decode
    (result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x81 in position 0: invalid start byte

另外一个好处是,明确配置为使用强制目标区域设置之一的环境将隐式获得目前在 C 区域设置中默认启用的编码透明行为。

在 UTF-8 区域设置强制期间避免设置 PYTHONIOENCODING

本 PEP 的早期版本提出将 PYTHONIOENCODING 设置为 utf-8:surrogateescape,而不是在解释器初始化期间更改标准流的默认处理方式。结果发现这会造成严重的兼容性问题:由于 surrogateescape 处理程序仅存在于 Python 3.1+ 中,因此在该配置下在子进程中运行 Python 2.7 进程可能会以令人困惑的方式中断。

当前的设计意味着早期 Python 版本将保留其标准流上的默认 strict 错误处理,而 Python 3.7+ 将始终使用更宽松的 surrogateescape 处理程序,即使这些区域设置是显式配置的(而不是通过区域设置强制达到的)。

放弃对传统 C 区域设置中基于 ASCII 的文本处理的官方支持

我们一直在努力使传统 C 区域设置中的严格字节/文本分离可靠工作,至今已超过十年。我们不仅未能使其工作,其他人也未能做到——唯一可行的替代方案是逐字传递字节而不急于将其解码为文本(C/C++、Python 2.x、Ruby 等),或者在很大程度上忽略名义上的 C/C++ 区域设置编码并假定使用 UTF-8(PEP 540、Rust、Go、Node.js 等)或 UTF-16-LE (JVM, .NET CLR)。

尽管本 PEP 确保真正需要这样做的开发人员仍然可以选择在传统 C 区域设置中运行他们的 Python 代码(通过设置 LC_ALL=CPYTHONCOERCECLOCALE=0,或运行设置 --without-c-locale-coercion 的自定义构建),它也明确表示我们期望 Python 3 的 Unicode 处理在该配置中完全可靠,建议的替代方案是使用更合适的区域设置(如果可用,可能与 PEP 540 的 UTF-8 模式结合使用)。

仅在独立运行时提供隐式区域设置强制

本 PEP 中提议的设计的主要缺点是,它在 CPython 运行时作为独立应用程序运行和作为嵌入在更大系统(例如 Apache httpd 中运行的 mod_wsgi)中的嵌入式组件运行时之间引入了潜在的行为差异。

在 Python 3.x 开发过程中,曾多次尝试在 Python 解释器初始化时改进对错误区域设置的处理。出现的问题是,这最终在解释器启动过程中太晚了——命令行参数和环境变量内容等数据可能在调用 Py_Initialize 之前就已经从操作系统中检索并以错误的 ASCII 文本编码假设进行处理了。

那些不一致性造成的问题比相信操作系统声称 ASCII 是适用于操作系统接口的编码所造成的问题更难诊断和调试。即使对于默认的 CPython 二进制文件也是如此,更不用说将 CPython 嵌入为脚本引擎的更大 C/C++ 应用程序了。

本 PEP 中提出的方法通过在独立运行时将区域设置强制尽可能早地移到解释器启动序列中来解决这个问题:它直接在 C 级别的 main() 函数中发生,甚至在调用实现 CPython 解释器 CLI 功能的 Py_Main() 库函数之前。

然后,Py_Initialize API 只会在检测到使用 C 区域设置时发出显式警告(在 stderr 上),并依赖嵌入应用程序指定更合理的内容。

话说回来,本 PEP 的参考实现将大部分功能添加到共享库中,CLI 已更新为无条件调用两个新的私有 API

if (_Py_LegacyLocaleDetected()) {
    _Py_CoerceLegacyLocale();
}

这些类似于其他旨在用于嵌入式应用程序的“预配置”API:它们设计用于在 Py_Initialize 之前调用,从而改变解释器的初始化方式。

如果这些被公开(作为本 PEP 的一部分或在后续的 RFE 中),那么其他嵌入式应用程序将很容易重新创建与 CPython CLI 相同的行为。

允许恢复传统行为

CPython 命令行解释器通常用于调查嵌入 CPython 的其他应用程序中发生的故障,即使在本 PEP 实施后,这些应用程序仍可能使用 C 区域设置。

为区域设置强制行为提供简单的开/关开关,可以更容易地重现此类应用程序的行为以进行调试,并且即使运行应用了此更改的版本,也可以更容易地重现旧 3.x 运行时的行为。

查询 LC_CTYPE 进行 C 区域设置检测

LC_CTYPE 是 CPython 依赖的实际区域设置类别,用于驱动环境变量、命令行参数和从操作系统接收的其他文本值的隐式解码。

因此,在尝试确定当前区域设置配置是否可能导致 Unicode 处理问题时,专门检查它是有意义的。

显式设置 LC_CTYPE 进行 UTF-8 区域设置强制

Python 经常被用作胶水语言,将当前进程中其他 C/C++ ABI 兼容组件以及子进程中用任意语言编写的组件集成起来。

LC_CTYPE 设置为 C.UTF-8 对于处理问题源于在未定义 UTF-8 区域设置的系统上提供了 LC_CTYPE=UTF-8 等设置的情况很重要(例如,当 Mac OS X ssh 客户端配置为转发区域设置,并且用户登录到 Linux 服务器时)。

这应该足以确保在激活区域设置强制时,切换到基于 UTF-8 的区域设置将一致地应用于当前进程和任何继承当前环境的子进程。

在 UTF-8 区域设置强制期间避免设置 LANG

本 PEP 的早期版本除了设置 LC_CTYPE 外,还提议设置 LANG 类别独立默认区域设置。

后来将其删除,理由是仅设置 LC_CTYPE 足以处理 PEP 旨在解决的所有问题场景,而同时设置 LANG 会破坏 LANG 设置正确但区域设置问题仅由不正确的 LC_CTYPE 设置引起的情况([22])。

例如,考虑一个调用 Linux date 工具的 Python 应用程序,而不是自己进行日期格式化

$ LANG=ja_JP.UTF-8 LC_CTYPE=C date
2017年  5月 23日 火曜日 17:31:03 JST

$ LANG=ja_JP.UTF-8 LC_CTYPE=C.UTF-8 date  # Coercing only LC_CTYPE
2017年  5月 23日 火曜日 17:32:58 JST

$ LANG=C.UTF-8 LC_CTYPE=C.UTF-8 date  # Coercing both of LC_CTYPE and LANG
Tue May 23 17:31:10 JST 2017

只在 Python 进程中更新 LC_CTYPE,子进程将继续按预期运行。然而,如果 LANG 也被更新,那将有效地覆盖 LC_TIME 设置并使用错误的日期格式约定。

在 UTF-8 区域设置强制期间避免设置 LC_ALL

本 PEP 的早期版本除了设置 LC_CTYPE 外,还提议设置 LC_ALL 区域设置覆盖。

在确定仅设置 LC_CTYPELANG 足以涵盖 PEP 旨在涵盖的所有场景之后,此更改得以实施,因为它避免了在以下情况下造成任何问题

$ LANG=C LC_MONETARY=ja_JP.utf8 ./python -c \
  "from locale import setlocale, LC_ALL, currency; setlocale(LC_ALL, ''); print(currency(1e6))"
¥1000000

如果当前环境中设置了 LC_ALL,则跳过区域设置强制

由于区域设置强制现在只设置 LC_CTYPELANG,因此如果 LC_ALL 也已设置,则不会产生任何影响。为避免在这种情况下发出虚假的区域设置强制通知,强制将完全跳过。

独立于“UTF-8 模式”考虑区域设置强制

鉴于本 PEP 的区域设置强制和 PEP 540 的 UTF-8 模式都在 Python 3.7 的考虑范围之内,有必要问一下我们是否可以只做其中一项,而不是同时进行这两项更改。

PEP 540 中提出的 UTF-8 模式有两个主要限制,使其成为本 PEP 的潜在补充而不是潜在替代。

首先,与本 PEP 不同,PEP 540 的 UTF-8 模式使得更改目前根本无法配置的默认行为成为可能。虽然这正是该提案有趣之处,但这也是它完全未经证实的原因。相比之下,本 PEP 中提出的方法直接建立在 C 区域设置系统(LC_CTYPELANG)和 Python 标准流(PYTHONIOENCODING)的现有配置设置之上,这些设置已经使用了多年来处理本 PEP 中讨论的兼容性问题。

其次,根据经验,我们知道拟议的区域设置强制不仅可以解决 CPython 本身的问题,还可以解决与标准流交互的扩展模块(如 GNU readline)中的问题。例如,考虑一个启用了 PEP 538 的 CPython 构建中的以下交互式会话,其中第一行之后的所有行都是通过“向上箭头,向左箭头 x4,删除,回车”执行的

$ LANG=C ./python
Python 3.7.0a0 (heads/pep538-coerce-c-locale:188e780, May  7 2017, 00:21:13)
[GCC 6.3.1 20161221 (Red Hat 6.3.1-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print("ℙƴ☂ℌøἤ")
ℙƴ☂ℌøἤ
>>> print("ℙƴ☂ℌἤ")
ℙƴ☂ℌἤ
>>> print("ℙƴ☂ἤ")
ℙƴ☂ἤ
>>> print("ℙƴἤ")
ℙƴἤ
>>> print("ℙἤ")
ℙἤ
>>> print("ἤ")
ἤ
>>>

这正是我们对一个行为良好的命令历史编辑器的期望。

相比之下,以下是旧版本中当前发生的情况,如果您只更改 Python 级别的流编码设置而不更新区域设置

$ LANG=C PYTHONIOENCODING=utf-8:surrogateescape python3
Python 3.5.3 (default, Apr 24 2017, 13:32:13)
[GCC 6.3.1 20161221 (Red Hat 6.3.1-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print("ℙƴ☂ℌøἤ")
ℙƴ☂ℌøἤ
>>> print("ℙƴ☂ℌ�")
 File "<stdin>", line 0

   ^
SyntaxError: 'utf-8' codec can't decode bytes in position 20-21:
invalid continuation byte

这种特定的错误行为来自 GNU readline,而不是 CPython——因为命令历史编辑不是 UTF-8 感知的,它损坏了历史缓冲区并向 stdin 提供了这种无稽之谈,以至于甚至绕过了 surrogateescape 错误处理程序。虽然 PEP 540 的 UTF-8 模式在技术上可以更新以重新配置 readline,但这只是一个可能在不通过 CPython C API 的情况下与标准流交互的扩展模块,并且 CPython 所做的任何更改都只在 readline 直接作为 Python 3.7 的一部分运行时才适用,而不是在单独的子进程中运行。

但是,如果我们确实更改了配置的区域设置,GNU readline 将开始正常运行,而无需对嵌入应用程序进行任何更改

$ LANG=C.UTF-8 python3
Python 3.5.3 (default, Apr 24 2017, 13:32:13)
[GCC 6.3.1 20161221 (Red Hat 6.3.1-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print("ℙƴ☂ℌøἤ")
ℙƴ☂ℌøἤ
>>> print("ℙƴ☂ℌἤ")
ℙƴ☂ℌἤ
>>> print("ℙƴ☂ἤ")
ℙƴ☂ἤ
>>> print("ℙƴἤ")
ℙƴἤ
>>> print("ℙἤ")
ℙἤ
>>> print("ἤ")
ἤ
>>>
$ LC_CTYPE=C.UTF-8 python3
Python 3.5.3 (default, Apr 24 2017, 13:32:13)
[GCC 6.3.1 20161221 (Red Hat 6.3.1-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print("ℙƴ☂ℌøἤ")
ℙƴ☂ℌøἤ
>>> print("ℙƴ☂ℌἤ")
ℙƴ☂ℌἤ
>>> print("ℙƴ☂ἤ")
ℙƴ☂ἤ
>>> print("ℙƴἤ")
ℙƴἤ
>>> print("ℙἤ")
ℙἤ
>>> print("ἤ")
ἤ
>>>

在 Mac OS X、iOS 和 Android 上启用 C 区域设置强制和警告

在 Mac OS X、iOS 和 Android 上,CPython 已经假定系统接口使用 UTF-8,我们预计大多数其他区域设置感知组件也会这样做。

因此,本 PEP 最初提议在构建时禁用这些平台的区域设置强制和警告,假设这将是完全冗余的。

然而,这个假设被证明是不正确的,因为随后的调查显示,如果您在这些平台上明确配置 LANG=C,像 GNU readline 这样的扩展模块将像在其他 *nix 系统上一样表现不佳。[21]

此外,Mac OS X 也经常被用作 Python 软件的开发和测试平台,这些软件旨在部署到其他 *nix 环境(例如 Linux 或 Android),而 Linux 也同样经常被用作移动和 Mac OS X 应用程序的开发和测试平台。

因此,本 PEP 默认在所有使用 CPython 的 autotools 构建工具链的平台上(即除 Windows 之外的所有地方)启用区域设置强制和警告功能。

实施

参考实现正在 Alyssa Coghlan 的 GitHub 上的 CPython 存储库分支 pep538-coerce-c-locale 功能分支中开发 [18]。一个正在进行中的 PR 可在 [20] 处找到。

此参考实现不仅涵盖了问题 28180 中的增强请求 [1],还涵盖了解决问题 28997 所需的 Android 兼容性修复 [16]

回溯到更早的 Python 3 版本

回溯到 Python 3.6.x

如果本 PEP 被 Python 3.7 接受,则鼓励和允许再分发商专门将其回溯到其初始 Python 3.6.x 版本。然而,此类回溯应仅在需要同时提供合适区域设置的更改的情况下进行,或者专门针对已经始终提供此类区域设置的平台。

至少 Fedora 项目正在计划在即将发布的 Fedora 26 版本中采用这种方法 [19]

回溯到其他 3.x 版本

尽管所提议的行为更改主要被视为解决 Python 3 在默认基于 ASCII 的 C 区域设置中当前错误行为的 bug 修复,但它仍然代表了 CPython 与 C 区域设置系统交互方式的一个相当重大的更改。因此,尽管一些再分发商仍可能根据其特定用户群的需求和兴趣选择将其回溯到更早的 Python 3.x 版本,但这不被鼓励作为一般做法。

然而,默认配置 Python 3 环境(例如基础容器镜像)以使用这些配置设置是允许和推荐的。

致谢

本 PEP 中提出的区域设置强制方法直接受到 Armin Ronacher 在 click 命令行实用程序开发框架中处理此问题的方式的启发 [2]

$ LANG=C python3 -c 'import click; cli = click.command()(lambda:None); cli()'
Traceback (most recent call last):
  ...
RuntimeError: Click will abort further execution because Python 3 was
configured to use ASCII as encoding for the environment.  Either run this
under Python 2 or consult http://click.pocoo.org/python3/ for mitigation
steps.

This system supports the C.UTF-8 locale which is recommended.
You might be able to resolve your issue by exporting the
following environment variables:

    export LC_ALL=C.UTF-8
    export LANG=C.UTF-8

该更改最初是作为 Fedora 系统 Python 3.6 包的下游补丁提出的 [3],然后被重新制定为 Python 3.7 的 PEP,并包含一个允许再分发商回溯到早期版本的章节。在开发上游补丁的同时,Charalampos Stratakis 一直在致力于 Fedora 26 的回溯并就提议更改的实际可行性提供反馈。

最初的草案已发布到 Python Linux SIG 进行讨论 [10],然后根据该讨论和 Victor Stinner 在 PEP 540 [11] 中的工作进行了修订。

本 PEP 中 Unicode 处理示例中使用的“ℙƴ☂ℌøἤ”字符串取自 Ned Batchelder 优秀的“实用 Unicode”演示文稿 [9]

Stephen Turnbull 长期以来对他在筑波大学 (筑波大学) 经常遇到的文本编码处理挑战提供了宝贵的见解。

参考资料


来源:https://github.com/python/peps/blob/main/peps/pep-0538.rst

最后修改:2025-02-01 08:59:27 GMT