當前位置:
首頁 > 知識 > 艱難的旅程:推進Python 3.7的UTF-8新模式

艱難的旅程:推進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編碼模型的歷史故事和邏輯:





    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



    喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

    本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


    請您繼續閱讀更多來自 Python程序員 的精彩文章:

    教程:Python量子計算入門
    Python中的9個「奇怪」的現象

    TAG:Python程序員 |