艰难的旅程:推进Python 3.7的UTF-8新模式

自从2008年python 3.0发布以来,每次有用户报告编码问题,一些人就会出来问为什么不“简单的”把UTF-8作为默认编码。好吧,事情没有这么简单。UTF-8在大部分情况下是最佳编码模式,但即使现在已经2018年,也并不是在所有情况下都适用。系统的当前编码依旧是Python默认编码的最好选择(对我来说,至少是问题最少的选择)。

这篇文章讲述了我对Python"添加UTF-8作为默认环境"的增强提案。此外,POSIX的本地环境已可以使用UTF-8模式:POSIX系统中Python3.7使用UTF-8作为默认环境。我的 PEP 540是对Nick Coghlan的PEP 538 的补充。

当我开始写这篇文章,我写了一些类似于"我添加了新的选项去使用UTF-8,享受它吧!"这类的话,而这样写使UTF-8看起来已经像一个普遍的选择,也让这份增强提案看起来很简单。不,没有什么事是明显的,也没有什么是简单的。

我花了一年的时间去设计和应用我的PEP 540,并让它被采纳。在此之前还写了五篇文章去展现PEP 540艰难的诞生之路,从Python3.0开始,到选择最佳的Python编码。我的这份提案是建立在之前工作基础上的。

这篇文章是本系列的第六篇也是最后一篇文章,用来讲述操作系统中Python编码模型的历史故事和逻辑:

Python 3.0 listdir() Bug on Undecodable Filenames

Python 3.1 surrogateescape error handler (PEP 383)

Python 3.2 Painful History of the Filesystem Encoding

Python 3.6 now uses UTF-8 on Windows

Python 3.7 and the POSIX locale

Python 3.7 UTF-8 Mode

本地环境编码失败,默认选择UTF-8?

2010年5月,我提交了bpo-8610:"Python3/POSIX: errors if file system encoding is None".我问到当本地环境编码失败时,应将什么作为默认编码。我提议UTF-8,I wrote:

UTF-8是个很好的选择:我打赌越来越多操作系统会采用UTF-8.

Mark-Andre评论道:

不,那是个不好的选择。Python一直遵循的传统是如果可能尽量避免猜测。只要我们还不能保证文件系统确实采用了UTF-8编码,还是使用ASCII更安全。不知道为什么这个原则没有应用在文件系统编码上。

在实践中,当未指定系统默认编码时Python已默认使用UTF-8。我在Python3.2的开发分支中提交了commit b744ba1d以使默认编码(UTF-8)更明显,但是在3.2版本发布之前,我移除了改动,commit e474309b(Oct 2010):

initfsencoding(): get_codeset() failure is now a fatal error

为避免乱码不要使用UTF-8.

为Windows添加UTF-8选项的提案

2016年8月,bpo-27781: 当Steve Dower正在进行将文件系统编码转成UTF-8的工作,我不确定Windows是否应该将UTF-8设为默认,我更支持做一个不向下兼容的内置选项。我当时写到:

如果你选择这个方向,我会在UNIX/BSD上添加转换接口。我考虑使用"-X utf8"避免改变命令行解释器。

如果我们对一个计划达成一致,我也愿意去写一个Python增强提案,来回答那些让我不厌其烦的问题和抱怨。

我又添加道:

我的意思是在UNIX/BSD上 python3 -X utf8 会强制 sys.getfilesystemencoding() 转到UTF-8,忽略当前环境的设定。

不过后来Steve选择在Windows上将默认编码改成UTF-8,我的-X utf8方法就在这个问题中被忽略了。

为POSIX本地环境添加utf8选项的提案

16年9月,Jan Niklas Hasse 开启了关于docker镜像的bpo-28180, "sys.getfilesystemencoding() should default to utf-8".

我再次重申了我的观点:

我提议添加 -X utf8 命令来使UNIX强制使用utf8编码,这对你来说可行吗?

Jan Niklas Hasse回答道:

不行,这意味着我要修改代码中所有的python调用,而且不能应用于可执行文件。

16年9月,我又回复道:

通常,我们在python中添加新选项时,会同时添加命令行选项(-X utf8)和 环境变量:我提议 PYTHONUTF8=1。

在你的docker容器中,用你喜欢的方式去定义‘系统级’的环境变量。

备注:技术上讲,我并不确定这是否可以通过PYTHONUTF8支持 -E 选项,因为 -E 来自命令行,而我们首先需要用编码解码命令行参数来解析这些选项....又是个先有鸡还是先有蛋的问题;-)

Nick Coghlan写了他的PEP538:"将C语言环境强制转换为基于UTF-8的语言环境",在2017年5月验证并在六月实施。

又一次,我关于UTF8的idea被忽略了。

我的 PEP540 第一个版本:添加一个新的UTF-8模式

17年一月,作为bpo-27781和bpo-28180的后续,我写了PEP 540: Add a new UTF-8 Mode并将它发到python-ideas和大家一起讨论。

简介:

添加新的UTF-8模式,加入选项以将UTF-8用于操作系统数据而不是区域编码。添加-X utf8命令行选项和PYTHONUTF8环境变量。

在十小时的交流之后,我写了第二个版本:

我修改了我的PEP:POSIX语言环境现在启用UTF-8模式。

INADA Naoki评论道:

我想默认启用UTF-8模式(内置退出选项),即使本地环境不是POSIX,如PYTHONLEGACYWINDOWSFSENCODING。

用户需要知道本地环境以及如何配置它。他们可以理解语言环境模式和UTF-8模式之间的区别,他们可以选择退出UTF-8模式。

但是很多人生活在“UTF-8无处不在”的世界里,并且不了解本地环境的情况。

始终忽略区域设置以始终使用UTF-8将是向后不兼容的更改。我没有勇气提出它,我只想提出一个内置选项,除了POSIX语言环境的特定情况。

不仅人们有不同的意见,而且大多数人对如何处理Unicode有强烈的意见,并没有做好妥协的准备。

PEP540的第三版本:

在经历了一周的时间、59封邮件的讨论之后,我实施了我的PEP540并写了提案的第三版本:

自PEP的第一个版本以来,我做了多处更改:

1.UTF-8严格模式现在仅对输入和输出使用严格:它保留了操作系统数据的代理。请阅读“使用操作系统数据的严格错误处理程序”替代方法。

2.POSIX语言环境现在启用UTF-8模式。有关基本原理,请参阅“不要修改POSIX语言环境的编码”替代方案。

3.指定-X utf8,PYTHONUTF8,PYTHONIOENCODING等之间的优先级。

PEP的第三个版本具有更长的基本原理和更多示例。(......)

这一阶段收到了19封邮件讨论,所以,总的来说这个月收到了78封邮件。与此同时,Nick Coghlan的PEP538也还在讨论当中。

沉默的一年

由于python-ideas线索的基调以及我不知道如何处理Nick Coghlan的PEP 538,我决定在一年内(2017年1月至12月)什么都不做。 2017年4月,尼克提议INADA Naoki担任他的PEP 538和我的PEP 540的BDFL代表。Guido接受了代表请求。

2017年5月,Naoki批准了Nick的PEP 538,然后尼克实施了它。

PEP540第三版发布到python-dev

2017年底,当我在Python 3.7的新内容中查看我在Python 3.7中所做的贡献时,我没有看到任何重大贡献。我想提出一些建议。此外,Python 3.7功能冻结(第一个测试版)的截止日期即将于2018年1月底结束。

17年12月,我决定进行下一步:我把提案发送到了python-dev的邮件列表.

Guido van Rossum抱怨PEP的长度:

我一直在与Victor离线讨论这个PEP,但他建议我们应该公开讨论它。 我非常担心这个漫长而漫无边际的PEP,我建议如果没有重大改写就不能接受,只关注规范的清晰度。 “Unicode just works”的总结更像是一个希望而不是PEP的正确摘要。

(...)

所以我猜PEP接受周结束了。 :-(

重写PEP

即使我并不完全相信自己的PEP是一个好主意,我也想得到正式投票,以了解我的想法是否应该被实施或放弃。我决定从头开始重写我的PEP:

PEP version 3 (before rewrite): 1,017 行

PEP version 4 (after rewrite): 263 行 (26% 是之前的版本)

我将理由简化为严格的最小值,以解释PEP的关键点:

1.本地环境编码和UTF-8

2.解决不能编码问题:surrogateescape错误解决机制

3.严格的UTF-8以确保正确性

4.默认情况下不会更改,以获得最佳向后兼容

使用surrogateescape读取JPEG图片

17年12月,我发送了更短的PEP第四版给python-dev

INADA Naoki指出了一个设计问题:

我现在有一点担忧,使用UTF-8模式,open()的默认编码/报错是UTF8/surrogateescape。

(...)

打开没有“b”选项的二进制文件是新开发人员非常常见的错误。如果默认错误处理程序是surrogateescape,他们就不会注意到他们的错误了。

他举了一个例子:

使用PEP 538(C.UTF-8语言环境),open()使用UTF-8 / strict,而不是UTF-8 / surrogateescape。

例如,如果文件是JPEG文件,则此代码使用PEP 538引发UnicodeDecodeError。

我回复道:

虽然我并不十分确信必须为surrogateescape更改open()的错误处理程序,但首先我想确定在更改它之前这是否是一个非常糟糕的主意:-)

(......)

使用JPEG图像,这个例子显然是错误的。 但是已经选择在open()上使用surrogateescape来读取大多数正确编码为UTF-8的文本文件,除了一些bytes文件。 我不知道如何解释这个问题。 Mercurial wiki页面有一个很好的例子,他们称之为“Makefile问题”。

Guido van Rossum说服了我:

你会很容易得到解码错误,这就是INADA的观点。(除非你使用encoding ="Latin-1")他担心的是surrogateescape错误处理程序使得你不会得到解码错误,然后失败后更难调试。

于是我写了我的PEP的第5版:

我对PEP 540进行了以下两项更改:

1.open()错误处理程序仍然是“严格”

2.删除不再有意义的“严格的UTF8模式”

关于locale.getpreferredencoding()的最后一个问题

17年12月,INADA Naoki 问道:

在UTF-8模式下,locale.getpreferredencoding()也返回"UTF-8"?

哦,这是一个很好的问题!我查看了代码并同意返回UTF-8:

我检查了stdlib,我发现很多地方使用locale.getpreferredencoding()来获取用户首选编码:

1. builtin open():默认编码

2.cgi.FieldStorage:对查询字符串进行编码

3.encoding._alias_mbcs():检查请求的编码是否是ANSI代码页

4.gettext.GNUTranslations:lgettext()和lngettext()方法

5.xml.etree.ElementTree:ElementTree.write(encoding ="unicode")

在UTF-8模式下,我希望cgi,gettext和xml.etree都默认使用UTF-8编码。因此,如果启用了UTF-8模式,locale.getpreferredencoding()应该返回UTF-8。

我发送了第六版的PEP:

在UTF-8模式下,locale.getpreferredencoding()也返回"UTF-8"。

此外,我还写了一篇“与场所强制的关系(PEP 538)”部分取代了“附件:PEP 538和PEP 540之间的差异”部分。许多人对PEP 538和PEP 540之间的关系感到困惑,要求了解新的部分。 最后,在第一个PEP版本发布一年后,INADA Naoki批准了我的PEP!

第一次不完整的部署

我于2017年3月开始着手实施PEP 540。一旦PEP获得批准,我就请INADA Naoki进行审核。他让我修复命令行解析以正确处理-X utf8选项:

当找到-X utf8选项时,我们可以再次从char **argv解码。由于mbstowcs()不保证循环跳转,因此优于对wchar_t **argv重新编码。

正确实现-X utf8选项是需要技巧性的。解析命令行是在wchar_t* C字符串(Unicode)上完成的,这需要解码字节字符串(bytes)的char** argv C数组。Python首先解码语言环境编码中的字节字符串。如果检测到utf8选项,则必须再次解码argv字节字符串,但现在必须用UTF-8解码。问题是代码并不是为此而设计的,它需要在Py_Main()中重构很多代码。

我回复道:

main()和Py_Main()非常复杂。随着PEP 432的提出,Nick Coghlan,Eric Snow和我正在努力使这个代码变得更好。参见例如bpo-32030。

(...)

出于所有的这些原因,我建议合并这个不完整的PR并为最复杂的部分编写不同的PR,重新编码wchar_t *命令行参数,实现Py_UnixMain()或其他更好的选项?

我想尽快让我的代码合并,以确保它将进入第一个Python 3.7测试版,以便在Python 3.7 final之前获得更长的测试时间。

2017年12月,bpo-29240,我推动了我的提交91106cd9:

PEP 540:添加新的UTF-8模式

2.locale.getpreferredencoding()现在在UTF-8模式下返回"UTF-8"。作为副作用,open()现在默认在此模式下使用UTF-8编码。

将Py_Main()拆分为子函数

2017年11月,我创建了bpo-32030,将大的Py_Main()函数拆分为更小的子函数。

我的目的是能够正确实施我的PEP540。我将花费3个月的时间和45次提交来完全清理Py_Main(),并将几乎所有Python配置选项放入私有C _PyCoreConfig结构中。

使用-X utf8时再次解析命令行

2017年12月,bpo-32030,由于Py_Main()重构,我能够完成我的PEP的实现。

我推动了我的提交9454060e:

1.如果编码改变,Py_Main()重新读取配置

2.如果编码改变(C语言环境强制或UTF-8模式改变),Py_Main()现在再次使用新编码读取配置。

如果在读取Python配置后更改了编码,请清除配置并使用新编码再次读取配置。重构允许的关键特性是能够正确清理所有配置。

UTF-8模式和语言环境编码

2018年1月,在处理bpo-31900时,“localeconv()应解码LC_NUMERIC编码的数字字段,而不是LC_CTYPE编码”,我测试了各种语言环境和编码组合。我发现了UTF-8模式的bug。

当-X utf8明确启用UTF-8模式时,意图是“无处不在”的使用UTF-8。对。但是有一些地方,实际已经应用的编码就是正确的编码,如time.strftime()函数。

bpo-29240:我推了第一个修复,提交cb3ae558:

忽略time模块中的UTF-8模式

time.strftime()必须使用当前的LC_CTYPE编码,如果启用了UTF-8模式,则不能使用UTF-8。 我测试了更多的案例,发现了......更多的错误。如果启用了UTF-8模式,则更多功能必须使用其当前的语言环境编码,而不是UTF-8。

我推了第二个修复,提交7ed7aead:

修复UTF-8模式下的语言环境编码

修改locale.localeconv(),time.tzname,os.strerror()和其他函数以忽略UTF-8模式:始终使用当前的语言环境编码。

第二个修复记录了公共C函数Py_DecodeLocale()和Py_EncodeLocale()使用的编码:

编码级别,最高优先级到最低优先级:

1.macOS和Android上的UTF-8;

2.如果启用了Python UTF-8模式,则为UTF-8;

3.如果LC_CTYPE语言环境为“C”,则为ASCII,nl_langinfo(CODESET)返回ASCII编码(或别名),mbstowcs()和wcstombs()函数使用ISO-8859-1编码。

4.当前的语言环境编码。

这个修复程序很复杂,因为我必须扩展Py_DecodeLocale()和Py_EncodeLocale()以在内部支持严格的错误处理程序。我还扩展到API以在失败时报告错误消息。

例如,Py_DecodeLocale()有原型:

而新的扩展和更通用的_Py_DecodeLocaleEx()有一个更复杂的原型:

要解码,有两个主要用例:

1.(FILENAME)如果启用了UTF-8模式,则使用UTF-8,否则使用语言环境编码。

2.有关确切使用的编码,请参阅Py_DecodeLocale()文档,事实更为复杂。(LOCALE)始终使用当前的区域设置编码

(FILENAME)示例:

1.Py_DecodeLocale(),PyUnicode_DecodeFSDefaultAndSize():使用surrogateescape错误处理程序

2.os.fsdecode()

3.os.listdir()

4.os.environ sys.argv中 等等

(LOCALE)示例:

1.PyUnicode_DecodeLocale():错误处理程序作为参数传递,必须是strict或surrogateescape

2.time.strftime()

3.locale.localeconv()

4.time.tzname os.strerror()

5.readline模块:内部decode()函数 等等

总结一下PEP540的发布历史

版本1:第一个版本发送到python-ideas

版本2:POSIX语言环境现在可以启用UTF-8模式

版本3:UTF-8严格模式现在仅对输入和输出使用严格错误处理程序

版本4:PEP从头开始重写,更加简化

版本5:open()错误处理程序仍然严格,并且已删除“严格的UTF8模式”

版本6:locale.getpreferredencoding()在UTF-8模式下return "UTF-8"。

最终批准的PEP总结:

添加新的“UTF-8模式”以增强Python对UTF-8的使用。当UTF-8模式处于活动状态时,Python将:

使用utf-8编码,无论当前平台当前设置的语言环境如何,以及将stdin和stdout错误处理程序更改为surrogateescape。

默认情况下,此模式处于关闭状态,但在使用“POSIX”语言环境时会自动激活。

添加-X utf8命令行选项和PYTHONUTF8环境变量以控制UTF-8模式。

总结…

现在是时候休息了......直到Python中再次出现重大的Unicode问题。

英文原文:https://vstinner.github.io/python37-new-utf8-mode.html

译者:XTH

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180908A0ABOK00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券