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-01-03、2017-01-07、2017-03-05、2017-05-09
决议:
Python-Dev 消息

目录

摘要

Python 3 在 *nix 系统上一个持续存在的挑战是,需要默认使用配置的区域设置编码以与同一进程或子进程中的其他区域设置感知组件保持一致,以及标准 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 区域设置是使用 UTF-8 作为 LC_CTYPE 类别的完整区域设置定义,以及与 C 区域设置相同的设置用于所有其他类别(包括 LC_COLLATE)。许多 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 提议使用最近定义的区域设置(使用 UTF-8 作为其默认文本编码)覆盖旧版 C 区域设置。这意味着文本编码覆盖不仅适用于 CPython,还适用于加载到当前进程中的任何区域设置感知扩展模块,以及从父进程继承其环境的子进程中调用的区域设置感知应用程序。这种方法旨在保留 CPython 传统上对与其他区域设置感知组件集成的强大支持,同时积极帮助推动在更广泛的 C/C++ 生态系统中采用和标准化 C.UTF-8 区域设置作为旧版 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 区域设置的别名。

要使 Python 3(无论确切版本如何)在基于 Fedora 和 Debian 的容器中表现得合理,最简单的方法是在这两个发行版都提供的 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 区域设置能够被像 CPython [6] 这样的基于 glibc 的应用程序普遍使用,但这不幸地不适用于那些提供旧版 glibc 版本(没有该功能)并且不提供 C.UTF-8(或等效项)作为磁盘区域设置(如 Debian 和 Fedora 所做的那样)的平台。这些平台在本 PEP 中被视为超出范围 - 请参阅 PEP 540 以进一步讨论在这些环境中改进 CPython 默认行为的可能选项。

设计原则

以上动机导致了以下提议解决方案的核心设计原则。

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

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

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

最大程度地减少对已正确配置为使用显式区域设置类别(如 LC_TIMELC_MONETARYLC_NUMERIC)但以其他方式在旧版 C 区域设置中运行的系统和程序的不利影响,给出了以下设计原则。

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

最后,维护与在编排用例中运行任意子进程的兼容性导致了以下设计原则。

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

规范

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

它还提议在语言运行时本身初始化时 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 设置任何特定的错误处理程序时,CPython 默认在标准流(sys.stdinsys.stdout)上使用 surrogateescape。对于其他区域设置,标准流的默认错误处理程序为 strict

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

sys.stderr 的默认错误处理程序没有提出任何更改:它将继续为 backslashreplace

Android 上的区域设置更改

独立于此 PEP 中的其他更改,Android 系统上的 CPython 将更新为调用 setlocale(LC_ALL, "C.UTF-8")(它当前调用 setlocale(LC_ALL, ""))和 setlocale(LC_CTYPE, "C.UTF-8")(它当前调用 setlocale(LC_CTYPE, ""))。

此特定于 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
鈩櫰粹槀鈩屆羔激
ℙƴ☂ℌøἤ

这反转了问题,使得 GB-18030 文件正确呈现,但 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 确保真正需要这样做的开发人员仍然可以选择在其 Python 代码中使用传统的 C 区域设置(通过设置 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 解释器初始化时处理不正确的区域设置。出现的问题是,这最终在解释器启动过程中太迟了 - 命令行参数和环境变量内容等数据可能已经从操作系统中检索并根据不正确的 ASCII 文本编码假设在 Py_Initialize 被调用之前很久就已处理完毕。

那些不一致性造成的问题比相信操作系统关于 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 的区域设置将在当前进程和继承当前环境的任何子进程中一致地应用。

避免设置 LANG 以进行 UTF-8 区域设置强制

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

后来,考虑到仅设置 LC_CTYPE 就足以处理 PEP 旨在解决的所有问题场景,而同时设置 LANG 则会破坏 LANG 设置正确的场景,并且区域设置问题仅仅是由于 LC_CTYPE 设置不正确导致的 ([22]),因此该建议被移除。

例如,考虑一个 Python 应用程序,它在子进程中调用 Linux date 实用程序,而不是自己进行日期格式化。

$ 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 设置并使用错误的日期格式约定。

避免设置 LC_ALL 以进行 UTF-8 区域设置强制

本 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,它破坏了历史缓冲区,并将此类无意义的内容馈送到标准输入,甚至绕过了 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 也经常用作打算部署到其他 *nix 环境(如 Linux 或 Android)的 Python 软件的开发和测试平台,而 Linux 同样也经常用作移动和 Mac OS X 应用程序的开发和测试平台。

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

实现

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

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

向早期 Python 3 版本移植

向 Python 3.6.x 移植

如果本 PEP 被接受用于 Python 3.7,则重新分发者将被允许和鼓励专门将其更改反向移植到其初始 Python 3.6.x 版本中。但是,此类反向移植应仅与提供合适的默认区域设置所需的更改一起进行,或者专门针对此类区域设置已一致可用的平台进行。

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

向其他 3.x 版本移植

虽然提议的行为更改主要被视为修复 Python 3 在默认基于 ASCII 的 C 区域设置中的当前错误行为的错误修复,但它仍然代表了 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 优秀的“Pragmatic Unicode”演示文稿 [9]

Stephen Turnbull 长期以来一直为他在筑波大学遇到的文本编码处理挑战提供了宝贵的见解。

参考文献


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

上次修改:2023-10-11 12:05:51 GMT