Py学习  »  Python

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

Python程序员 • 5 年前 • 374 次点击  

自从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编码模型的历史故事和逻辑:

    1. Python 3.0 listdir() Bug on Undecodable Filenames

    2. Python 3.1 surrogateescape error handler (PEP 383)

    3. Python 3.2 Painful History of the Filesystem Encoding

    4. Python 3.6 now uses UTF-8 on Windows

    5. Python 3.7 and the POSIX locale

    6. 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模式 

         1.添加-X utf8命令行选项,PYTHONUTF8环境变量和新的sys.flags.utf8_mode标志.

         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



    今天看啥 - 高品质阅读平台
    本文地址:http://www.jintiankansha.me/t/1vvyMbhxt2
    Python社区是高质量的Python/Django开发社区
    本文地址:http://www.python88.com/topic/23737
     
    374 次点击