[翻译]为什么sys.setdefaultencoding()会破坏代码

Posted mecforlove

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[翻译]为什么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

以上是关于[翻译]为什么sys.setdefaultencoding()会破坏代码的主要内容,如果未能解决你的问题,请参考以下文章

unicode编码系统翻译为什么有些在微信翻译不了

翻译翻译什么TMD叫EXPLAIN

翻译翻译什么TMD叫EXPLAIN

ANT的中文翻译是啥?

翻译: 漫画HTTPS原理一 为什么我们需要HTTPS

英语翻译