[翻譯]為什么sys.setdefaultencoding()會破壞代碼


注:本文2016-12-28發布於個人搭建博客,現在將內容遷移過來,會有些許表述改動,未經同意,請勿轉載。

原文Why sys.setdefaultencoding() will break code

我知道更聰明、更有經驗的Python程序員之前已經向python-dev提了相關問題,但每次當我需要向別人引用其中一個時,我發現很難找得到。今天當我在Google上搜索這個問題時,發現最相關的條目是我自己在2011年發給yum-devel的一個帖子。我知道以后我肯定有必要向別人證明不應該使用setdefaultencoding()方法,所以為了避免下次再去網絡上搜索,我決定在這里發表我的論證。

一些背景

15年以前:支持Unicode的Python問世

對Python2而言,一定程度下我們可以將字節串(str type)和字符串(unicode type)混為一談,例如:

>>> u'Toshio' == 'Toshio'
True
>>> print(u'Toshio' + ' Kuratomi')
Toshio Kuratomi

當你執行這些操作時,Python發現一邊是unicode類型,另一邊是str類型,於是它取出str的值,將其解碼成一個unicode類型,然后繼續執行相應的操作。解析這些字節碼的編碼就是我們說的defaultencoding(根據sys.getdefaultencoding()命名,通過這個函數你可以查看當前的默認編碼)。

當Python開發者第一次試驗與str截然不同的unicode字符串時,他們不確定defaultencoding應該設置成什么。因此他們創建了sys.setdefaultencoding方法,這個方法在Python程序啟動時被調用以試驗不同的defaultencoding值帶來的不同影響。Python的作者們通過改變自己的site.py文件,觀察設置不同的默認編碼對代碼行為的影響,從而獲取更多經驗。

最終在2000年8月(寫本文時已經過去了14年半),上述的Python試驗版本正式成為Python-2.0,它的作者們決定將這個敏感的配置defaultencoding設置成ascii。

我知道今天再次去評價ascii的決定很容易,但是在14年以前字符編碼風格比今天更混亂。新出現的編程語言和API已經針對unicode固定兩個字節的編碼規則進行了優化。但是針對特定自然語言的非unicode一字節編碼在那時使用更加廣泛。許多數據(甚至在今天)可以包含非ascii文本,而不去聲明解碼方式。在那個年代,任何游離ascii編碼王國之外的人都需要被警醒:他們正進入一片編碼惡魔肆意游盪的土地。ascii在許多跨越邊界的情況下拋出錯誤,從而警告人們必須嚴加看管自己的代碼。

然而,在Python-2.0帶來unicode功能的同時,Python的作者們卻漸漸發現有一個疏忽帶來了很不好的影響。這個疏忽便是他們沒有刪除sys.setdefaultencoding()這個方法。為了彌補這個疏漏,他們在site.py中刪除了sys的這一屬性,從而避免人們在初始化以外的地方使用setdefaultencoding(),但是他們仍然可以在自己的site.py中改變defaultencoding。

sys.setdefaultencoding()的濫用

隨着時間的推移,utf-8編碼在Unix-like操作系統和網絡傳輸中占據着統治地位。很多只需處理utf-8編碼文本的人厭倦了字符串和字節串混在一起帶來的錯誤。於是他們發現了setdefaultencoding()這根稻草,開始嘗試用這種方式擺脫他們遇到的麻煩。

起初,有能力的程序員通過更新Python安裝的全局文件site.py來使用setdefaultencoding (),這也是Python官方文檔建議的用法,這只在用戶自己的機器上有用。不幸的是,這些用戶通常都是程序員,他們的程序需要在其他人的機器上運行,比如IT部門、客戶以及遍布整個互聯網的用戶。這意味着更新site.py文件會使他們處於比以前更糟糕的境地:他們的代碼在自己的機器上似乎工作良好,卻在正真使用該軟件的人那里運行奔潰。

由於程序員的關注點僅限於別人能否使用他們的軟件,所以他們認為如果自己的軟件可以將設置默認編碼作為其初始化的一部分,那事情就好辦多了。他們不必再強迫別人修改自己的Python安裝,因為他們的軟件會在運行時做出決定。於是乎他們重新審視了一下sys.setdefaultencoding()這個方法。雖然Python的作者們盡最大努力讓這個方法在python啟動后不可用,但程序員還是想到了獲取這個功能的妙方:

import sys
reload(sys)
sys.setdefaultencoding('utf-8')

一旦這段代碼運行,強制字節串轉換成字符串的的默認編碼將變為utf-8。這意味着當utf-8編碼的字節串與unicode字符串混合時,Python將成功地將str類型數據轉換為unicode類型,並將它們合並成一個unicode字符串。 這就是新一代的程序員對於他們大部分數據所期待的樣子,所以用這幾行(不可否認非常的hack)代碼解決問題的想法對他們來說非常有吸引力。 不幸的是,這樣做有很明顯的缺點。

為什么sys.setdefaultencoding()會破壞你的代碼

(1)編寫一次,改變一切

sys.setdefaultencoding()帶來的第一個問題乍一看不是很明顯。當你使用這個方法時,即將運行的代碼都將受到影響。你的代碼,標准庫的代碼以及不受你管控的第三方代碼都將在你設置的默認編碼下運行。有些不是你負責的代碼依賴的默認編碼是ascii,此時它就不會拋出錯誤,很可能制造一些垃圾數據。比如,你依賴的第三方庫有如下代碼:

def welcome_message(byte_string):
    try:
        return u"%s runs your business" % byte_string
    except UnicodeError:
        return u"%s runs your business" % unicode(byte_string,
            encoding=detect_encoding(byte_string))
 
print(welcome_message(u"Angstrom (Å®)".encode("latin-1"))

如果沒有改變默認編碼,這段代碼將無法通過ascii解碼"Å",隨后進入異常處理,猜測編碼並將其正確的轉換成unicode字符串,程序會打印出 Angstrom (Å®) runs your business。一旦你將defaultencoding設置為utf-8,代碼將使用utf-8解碼數據,打印Angstrom (Ů) runs your business

當然,如果這段代碼是在你自己的軟件中,你完全有能力去處理這個編碼問題。但是你並不能對第三方庫做這些事情。

(2)我們正破壞字典

設置utf-8為默認編碼帶來的最嚴重的問題是破壞了字典的一些行為約定。我們來看下面這段代碼:

def key_in_dict(key, dictionary):
    if key in dictionary:
        return True
    return False
 
def key_found_in_dict(key, dictionary):
    for dict_key in dictionary:
        if dict_key == key:
            return True
    return False

你認為輸入參數相同兩個函數的輸出會一致嗎?在Python中,如果你沒有濫用sys.setdefaultencoding()這個方法,那問題答案是肯定的。

>>> # Note: the following is the same as d = {'Café': 'test'} on
>>> # systems with a utf-8 locale
>>> d = { u'Café'.encode('utf-8'): 'test' }
>>> key_in_dict('Café', d)
True
>>> key_found_in_dict('Café', d)
True
>>> key_in_dict(u'Café', d)
False
>>> key_found_in_dict(u'Café', d)
__main__:1: UnicodeWarning: Unicode equal comparison failed to convert both arguments to Unicode - interpreting them as being unequal
False

但是如果我們使用sys.setdefaultencoding('utf-8') 又會發生什么呢?答案是上面的行為會遭到破壞:

>>> import sys
>>> reload(sys)
>>> sys.setdefaultencoding('utf-8')
>>> d = { u'Café'.encode('utf-8'): 'test' }
>>> key_in_dict('Café', d)
True
>>> key_found_in_dict('Café', d)
True
>>> key_in_dict(u'Café', d)
False
>>> key_found_in_dict(u'Café', d)
True

在使用in操作時,程序計算key的hash值然后對比hash值是否相等。在utf-8編碼下,只有在ascii編碼體系里的字符串的unicode和str的hash值是相等的,其他的字符集下字符串的unicode和str的hash值是不相等的。==則會將字節串解碼成unicode然后再比較二者。當你調用sys.setdefaultencoding('utf-8')后,你便允許字節串以utf-8的方式轉換成unicode,然后兩個字符串對比后發現相等。這樣做的后果是in==的測試產生了不同的結果,這與人們習慣的行為相差甚遠,大多數人認為這打破了語言的基本約定。

所以Python 3是如何修復這個問題的呢?

你或許已經知道Python 3將默認編碼從ascii轉變成utf-8,那它如何避免==in帶來的問題呢?答案是Python 3不再進行字節串(python3 bytes type)和字符串(python3 str type)之間的隱式轉碼了。由於這兩種類型現在是完全分離的,所以上文進行的“包含測試”和“相等測試”都會返回False

$ python3
>>> a = {'A': 1}
>>> b'A' in a
False
>>> b'A' == list(a.keys())[0]
False

起初在Python 2中,ascii編碼體系下字節串和字符串是相等的,這看起來有些滑稽。但是請記住字節串只是一種數字類型,下面的代碼並不能像你期望的那樣工作:

>>> a = {'1': 'one'}
>>> 1 in a
False
>>> 1 == list(a.keys())[0]
False


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM