在Python中尽量避免使用sys.setdefaultencoding(‘utf-8’)方法


=Start=

缘由:

之前在写与中文字符处理有关的Python代码中总喜欢使用:

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

这种方式来避免程序出错和异常,也遇到过一些文章说这样的方式不好,但一直没来得及细看说明和原因,这次有时间了整理一下(主要参考的是「立即停止使用 setdefaultencoding(‘utf-8’), 以及为什么」一文),方便以后参考。

正文:

参考解答:

The use of sys.setdefaultencoding() has always been discouraged, and it has become a no-op in py3k. The encoding of py3k is hard-wired to “utf-8” and changing it raises an error.

这个操作最大的问题在于它将会使得一些代码行为变得怪异,而且这怪异还不易识别、不好修复,以一个不可见的 bug 存在着。


1、可能导致的 dictionray 行为异常

假设我们要从一个 dictionary 里查找一个 key 是否存在,通常来说,有两种可行的方法:

d = {1:2, '1':'2', '你好': 'hello'}
def key_in_dict(key):
if key in d:
return True
return False

def key_found_in_dict(key):
for _key in d:
if _key == key:
return True
return False

接下来对比一下改变系统默认编码前后这俩函数的输出有什么不同:

print(key_in_dict('你好'))
print(key_in_dict(u'你好'))
print(key_found_in_dict(u'你好'))
'''
True
False
False #UnicodeWarning: Unicode equal comparison failed to convert both arguments to Unicode - interpreting them as being unequal
'''

print('------utf-8------')

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

print(key_in_dict('你好'))
print(key_in_dict(u'你好'))
print(key_found_in_dict(u'你好'))
'''
True
False
True
'''

可以看到,当默认编码改了之后,两个函数的输出不再一致(对于 key_found_in_dict 函数来说)。原因在于:

dict 的 in 操作符将键做哈希,并比较哈希值判断是否相等。对于 ascii 集合内的字符来说,不管是字节字符类型还是还是 unicode 类型,其哈希值是一样的,如 u'1' in {'1':1} 会返回 True而超出 ascii 码集的字符,如上例中的 '你好',它的字节字符类型的哈希与 unicode 类型的哈希是不一样的。

而 == 操作符则是做了一次转换,将字节字符(byte string,上面的 '你好')转换成 unicode(u'你好') 类型,然后对转换后的结果做比较。在 ascii 系统默认编码中,'你好'转换成 Unicode 会产生 Warning: UnicodeWarning: Unicode equal comparison failed to convert both arguments to Unicode - interpreting them as being unequal,因为超出码集无法转换,系统会默认其不相等。当系统编码被我们手动改为 utf-8 后,这个禁忌则被解除,'你好' 能够顺利被转换成 unicode,最后的结果就是,in 和 == 行为不再一致。

2、问题的根源:Python2 中的 string

Python 为了让其语法看上去简洁好用,做了很多 tricky 的事情,混淆 byte string 和 text string 就是其中一例。

在 Python 里,有三大类 string 类型,unicode(text string),str(byte string,二进制数据),basestring,是前两者的父类。

其实,在语言设计领域,一串字节(sequences of bytes)是否应该当做字符串(string)一直是存在争议的。我们熟知的 Java 和 C# 投了反对票,而 Python 则站在了支持者的阵营里。其实我们在很多情况下,给文本做的操作,比如正则匹配、字符替换等,对于字节来说是用不着的。而 Python 认为字节就是字符,所以他们俩的操作集合是一致的。

然后进一步的,Python 会在必要的情况下,尝试对字节做自动类型转换,例如,在上文中的 ==,或者字节和文本拼接时。如果没有一个编码(encoding),两个不同类型之间的转换是无法进行的,于是,Python 需要一个默认编码。在 Python2 诞生的年代,ASCII 是最流行的(可以这么说吧),于是 Python2 选择了 ASCII。然而,众所周知,在需要需要转换的场景,ASCII 都是没用的(128个字符,够什么吃)。

在历经这么多年吐槽后,Python 3 终于学乖了。默认编码是 Unicode,这也就意味着,做所有需要转换的场合,都能正确并成功的转换。

3、应对此类问题的最佳实践

方法一:直接使用/迁移至 Python 3 。

Python 3最重要的新特性大概要算是对文本和二进制数据作了更为清晰的区分。文本总是Unicode,由str类型表示,二进制数据则由bytes类型表示。Python 3不会以任意隐式的方式混用str和bytes,正是这使得两者的区分特别清晰。你不能拼接字符串和字节包,也无法在字节包里搜索字符串(反之亦然),也不能将字符串传入参数为字节包的函数(反之亦然)。这是件好事。

字符串可以编码encode()成字节包,而字节包可以解码decode()成字符串

方法二(在继续使用Python 2时的几个建议):

  • 所有 text string 都应该是 unicode 类型,而不是 str,如果你在操作 text,而类型却是 str,那就是在制造 bug。
  • 在需要转换的时候,显式转换。从字节解码成文本,用 var.decode(encoding),从文本编码成字节,用 var.encode(encoding)
  • 从外部读取数据时,默认它是字节,然后 decode 成需要的文本;同样的,当需要向外部发送文本时,encode 成字节再发送。
参考链接:

=END=


《“在Python中尽量避免使用sys.setdefaultencoding(‘utf-8’)方法”》 有 1 条评论

  1. UnicodeDecodeError: ‘utf8’ codec can’t decode byte 0xa5 in position 0: invalid start byte
    https://stackoverflow.com/questions/22216076/unicodedecodeerror-utf8-codec-cant-decode-byte-0xa5-in-position-0-invalid-s
    `
    decode(errors=’replace’) #Replace with a replacement marker. On encoding, use ? (ASCII character). On decoding, use � (U+FFFD, the official REPLACEMENT CHARACTER). Implemented in replace_errors().

    decode(errors=’ignore’) #Ignore the malformed data and continue without further notice. Implemented in ignore_errors().

    f = open(“file.txt”, “rb”)
    text = f.read().decode(errors=’replace’)

    with open(out_file, ‘rb’) as f:
    for line in f:
    print(line.decode(errors=’ignore’))
    `

    codecs.decode
    https://docs.python.org/3/library/codecs.html#codecs.decode

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注