艱難的旅程:推進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

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

本地環境編碼失敗,默認選擇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有原型:

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

而新的擴展和更通用的_Py_DecodeLocaleEx有一個更復雜的原型:

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

要解碼,有兩個主要用例:

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


分享到:


相關文章: