艱難的旅程:推進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模式
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中的9個「奇怪」的現象
TAG:Python程序員 |