关于 Python 2 / 3 字符编码(中文字符乱码或编码错误等等)

一 、Python 3 默认使用 UTF-8 ,完美兼容中文字符

因为 Python 3 的字符编码格式默认为 UTF-8 ,所以在使用 Python 3 进行编程的时候,基本上是不会出现中文字符乱码或者编码错误等等一系列问题的。如下例所示,我们将字符串 “ 你好。Hello, World! ” 存入 somefile.txt 中,然后再读取出来:

# 如果文件存在,则删除

import os
if os.path.exists('somefile.txt'):
    os.remove('somefile.txt')

# 将字符串写入文件

f = open('somefile.txt', 'w')
f.write('你好。Hello, ')
f.write('World!')
f.close()

# 从文件读取字符串

#f = open('somefile.txt', 'r')
f = open('somefile.txt')
print(f.read())
f.close()

在 Python 3.8.0 中运行上述代码,中文能正确打印出来,运行结果如下所示:

在 Python 3.8.0 中运行上述代码,中文能正确打印出来
在 Python 3.8.0 中运行上述代码,中文能正确打印出来

如果我们显式使用 Unicode 字符串也是没有问题的:

# 如果文件存在,则删除

import os
if os.path.exists('somefile.txt'):
    os.remove('somefile.txt')

# 将字符串写入文件

f = open('somefile.txt', 'w')
f.write(u'你好。Hello, ')
f.write(u'World!')
f.close()

# 从文件读取字符串

#f = open('somefile.txt', 'r')
f = open('somefile.txt')
print(f.read())
f.close()

运行结果同上图。

Python 3 中 u’你好。Hello, ‘‘你好。Hello, ‘ 其实已经是一回事儿了,为什么呢?在解释这个问题之前,需要先科普一下 Unicode 和 UTF-8 之间的区别,以及 Python 2 和 Python 3 中都有哪些字符串类型。

Unicode 和 UTF-8 之间的区别

Unicode 为世界上所有字符都分配了一个唯一的数字编号,这个编号范围从 0x000000 到 0x10FFFF(十六进制),有 110 多万,每个字符都有一个唯一的 Unicode 编号,这个编号一般写成十六进制,在前面加上 U+  ,例如:“ 马 ” 的 Unicode 是 U+9A6C 。Unicode 就相当于一张表,建立了字符与编号之间的联系。

Unicode 是一种规定,Unicode 本身只规定了每个字符的数字编号是多少,并没有规定这个编号如何存储。有的人会说那我可以直接把 Unicode 编号转换成二进制进行存储,是的完全可以,但除了这种直接转换成二进制的方案外,还有其他方案,主要有 UTF-8 、UTF-16 和 UTF-32 。换句话说,UTF-8 、UTF-16 和 UTF-32 这些字符编码都是 Unicode 的一种实现方式。

Python 2 和 Python 3 中的字符串类型

在 Python 2 中有三种字符串类型:str 类型的字符串( byte string ,字节字符串)、Unicode 类型的字符串( text string ,文本字符串)和 basestring(它是前两者的父类)。像 ‘你好’ 这种普通的字符串就属于 str 类型的字符串,而像 u’你好’ 这种显式声明为采用 Unicode 字符编码的字符串就属于 Unicode 类型的字符串。我们来看一个例子:

# byte string(字节字符串,str 类型)
s = '你好'

# text string(文本字符串,Unicode 类型)
u = u'你好'

print isinstance(s, str)
print isinstance(u, unicode)

print s.__class__
print u.__class__

运行结果如下所示:

str 类型和 unicode 类型
str 类型和 unicode 类型

变量 s 是 str 类型,变量 u 是 Unicode 类型。

在 Python 3 中,Unicode 类型的字符串被取消了,常用的仅有 str 类型的字符串。我们来看一个例子:

s = '你好'
u = u'你好'

print(isinstance(s, str))
print(isinstance(u, str))

print(s.__class__)
print(u.__class__)

运行结果如下所示:

Python 3 中有 str 类型,但没有 unicode 类型了
Python 3 中有 str 类型,但没有 unicode 类型了

在 Python 2 中,u’你好。Hello, ‘ 就是 Unicode 类型的字符串,‘你好。Hello, ‘ 就是 str 类型的字符串;但到了 Python 3 中,它们都是一回事儿,都是默认采用 UTF-8 字符编码的 str 类型的字符串。

我们再接着看一个 Python 3 上的例子。Ricky 现在使用的操作系统是 MacOS 10.15.2 Public Beta 3 ,在 Python 3.8.0 的 IDLE 中输入:

print('Hello World')
print('你好世界')

中文字符可以被正确打印出来,没有问题:

Python 3.8.0 中打印中文不会出现问题
Python 3.8.0 中打印中文不会出现问题

二 、Python 2 默认使用 ASCII ,使用中文字符可能会出现问题

因为 Python 2 的字符编码格式默认为 ASCII ,所以理论上是无法兼容中文字符的。

为什么 Python 2 不将默认的字符编码格式设置为 Unicode 或者 UTF-8 呢?因为 Python 的诞生比 Unicode 标准发布的时间还要早,所以最早的 Python 只支持 ASCII 编码,普通的字符串 ‘ ABC ‘ 在 Python 内部都是 ASCII 编码的。之后的 Python 开始支持其他的字符编码,到了 Python 3 才将默认的字符编码格式设置为 UTF-8 。

现在最新的 Python 2 版本(当前最新版为 Python 2.7.17 )默认的字符编码格式还是 ASCII ,但遇到中文字符时会自动使用 UTF-8 这样的字符编码格式(也有可能是其他的字符编码格式,如 GB2312 ),也就是说 Python 2 现在在一定程度上支持中文字符了。我们来看一个例子:

print repr('你好')
print repr(u'你好')

运行结果如下所示:

Python 2.7.17 的中文字符编码
Python 2.7.17 的中文字符编码

我们可以看到,在 Python 2.7.17 中:

  • ‘你好’ 的编码为:’\xe4\xbd\xa0\xe5\xa5\xbd’ ,这显然是 UTF-8 字符编码,且是 str 类型的字符串
  • u’你好’ 的编码为:u’\u4f60\u597d’,这显然是 Unicode 字符编码,且是 Unicode 类型的字符串

大家可以自行查找一下,看看 UTF-8 编码下的 “ 你 ” 十六进制表示是不是:E4BDA0 ;然后再看看 Unicode 编码下的 “ 你 ” 十六进制表示是不是:4F60(或 U+4F60 )。

这里需要注意的是,虽然 UTF-8 是 Unicode 的一种实现方式,但是在 Python 2 中采用 UTF-8 字符编码的字符串不属于 Unicode 类型的字符串,而是属于 str 类型的字符串( byte string ,字节字符串):

  • Unicode 类型的字符串( text string ,文本字符串)就是直接把 Unicode 编号转换成二进制进行存储,不采用 UTF-8 、UTF-16 和 UTF-32 等等这些方案。
  • str 类型的字符串( byte string ,字节字符串)不仅可以用来存储采用 UTF-8 字符编码的字符串,还可以用来存储采用 GB2312 、GB18030 、GBK 以及 ASCII 等等字符编码的字符串。所谓 byte string 的意思就是说这是以字节为单位来存储的字符串(不是以字符为单位),跟具体的字符编码没有关系。

那 Python 2 如何知道这些 str 类型的字符串都采用什么字符编码呢?当 Python 2 不知道采用什么字符编码格式来解析字符串的时候,就会采用默认的字符编码格式(没错,这是个坑)。Python 2 默认的字符编码格式是 ASCII(当然也可以重新设置默认的字符编码格式,后文对此会有说明)。

跟 Python 3 中的例子一样,我们将字符串 “ 你好。Hello, World! ” 存入 somefile.txt 中,然后再读取出来:

# 如果文件存在,则删除

import os
if os.path.exists('somefile.txt'):
    os.remove('somefile.txt')

# 将字符串写入文件

f = open('somefile.txt', 'w')
f.write('你好。Hello, ')
f.write('World!')
f.close()

# 从文件读取字符串

#f = open('somefile.txt', 'r')
f = open('somefile.txt')
print(f.read())
f.close()

在 Python 2.7.17 中运行上述代码,中文能正确打印出来,运行结果如下所示:

在 Python 2.7.17 中运行上述代码,中文能正确打印出来
在 Python 2.7.17 中运行上述代码,中文能正确打印出来

看到这里,你一定会觉得 Python 2 现在完美支持中文字符了,没有什么问题了,对吧?但是如果我们显式使用 Unicode 字符串呢?

# 如果文件存在,则删除

import os
if os.path.exists('somefile.txt'):
    os.remove('somefile.txt')

# 将字符串写入文件

f = open('somefile.txt', 'w')
f.write(u'你好。Hello, ')
f.write(u'World!')
f.close()

# 从文件读取字符串

#f = open('somefile.txt', 'r')
f = open('somefile.txt')
print(f.read())
f.close()

运行结果如下图所示:

Python 2.7.17 中往文件写入 Unicode 字符串出现问题
Python 2.7.17 中往文件写入 Unicode 字符串出现问题

我们可以看到 Python 2.7.17 抛出了一个 UnicodeEncodeError 异常:

UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-2: ordinal not in range(128)

Unicode 编码错误:ASCII 字符编解码器无法对位置 0 – 2 中的字符进行编码:序号不在范围内( 128 )。

在执行 f.write() 方法时,会将 Unicode 类型的字符串转换为 str 类型的字符串。但在转换为 str 类型的字符串的时候,需要采用什么样的字符编码呢?此时默认的字符编码就派上用场了,Python 2 默认的字符编码就是 ASCII 。所以此时 Python 2 尝试将 u’你好。Hello, ‘ 转换为采用 ASCII 字符编码的 str 类型的字符串,而 ASCII 字符编码并不支持中文字符,所以就报了上面这个异常。

看来 Python 2 只是对中文字符做了兼容处理,毕竟默认的字符编码格式是 ASCII ,而 ASCII 是不支持中文字符的。还有一个例子也能说明这个问题,在 Python 2.7.17 的 IDLE 中输入:

print('Hello World')

IDLE 能正确打出英文字符串,但如果在 IDLE 中输入:

print('你好世界')

就出现问题了:

Python 2.7.17 中打印中文出现问题
Python 2.7.17 中打印中文出现问题

最坏实践

如果你不小心在 Python 2 中遇到了中文字符的编码错误问题,某度搜出来的文章基本都会告诉你这么做,在你的程序代码中加入以下三行:

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

上面这种代码曾经(现在依然)是解决中文编码的万能钥匙。解决编码错误问题一劳永逸,从此和下列异常:

UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-2: ordinal not in range(128)

说再见。

这么做的目的是为了将程序的默认字符编码设置为 UTF-8 :

import sys    # 导入 sys 模块
reload(sys)   # 重新载入 sys 模块
sys.setdefaultencoding('utf-8')    # 将程序的默认字符编码设置为 UTF-8

这里为什么非得要重新载入 sys 模块呢?因为自 Python 2.5 开始,在 sys 模块初始化后会人为删除 sys.setdefaultencoding() 这个方法,所以我们需要重新载入该模块。

似乎 Python 官方就不希望你去重新设置默认的字符编码,为什么呢?我们来看看在 Python 中加入 Unicode 支持的设计者和实现者: Marc-André Lemburg ,他在一个邮件列表上的回复:

The only supported default encodings in Python are:

Python 2.x: ASCII

Python 3.x: UTF-8

If you change these, you are on your own and strange things will
start to happen. The default encoding does not only affect
the translation between Python and the outside world, but also
all internal conversions between 8-bit strings and Unicode.

Hacks like what's happening in the pango module (setting the
default encoding to 'utf-8' by reloading the site module in
order to get the sys.setdefaultencoding() API back) are just
downright wrong and will cause serious problems since Unicode
objects cache their default encoded representation.

Please don't enable the use of a locale based default encoding.

If all you want to achieve is getting the encodings of
stdout and stdin correctly setup for pipes, you should
instead change the .encoding attribute of those (only).

--

Marc-Andre Lemburg

eGenix.com

从此可见,Python 2 唯一支持的内部编码只有 ASCII ,更改默认的字符编码为其它字符编码可能会导致各种各样奇怪的问题。在这里他也说了使用 sys.setdefaultencoding() 的方法是彻彻底底的错误。

那么接下来我们来看看到底会出现什么问题。

reload(sys) 导致的问题

如果你在程序代码的最上方加入了上述三行代码,同时在 IDLE 中运行 .py 文件,你会发现输出( print )看不到了,为什么呢?

因为 IDLE 作为一个 GUI Shell 环境,在启动初始化过程中,会设置特定的标准输入、标准输出和标准错误输出,使得输入和输出都在 IDLE 的 GUI Shell 中。而如果手动执行了 reload(sys) 以后,sys 模块的这三个变量将会被重置,导致输出无法显示在 IDLE 中。所以解决方案很简单,只需要在 reload 之前把这三个变量都复制一份,reload 之后再恢复回来就行了:

import sys
stdi,stdo,stde = sys.stdin,sys.stdout,sys.stderr
reload(sys)
sys.stdin,sys.stdout,sys.stderr = stdi,stdo,stde
sys.setdefaultencoding('utf-8')

我们回到刚才的例子,如果你想在 Python 2 中往文件里写入显式声明的 Unicode 字符串,你可以这么做:

import sys
stdi,stdo,stde = sys.stdin,sys.stdout,sys.stderr
reload(sys)
sys.stdin,sys.stdout,sys.stderr = stdi,stdo,stde
sys.setdefaultencoding('utf-8')

print(sys.getdefaultencoding())

# 删除文件
import os
if os.path.exists('somefile.txt'):
    os.remove('somefile.txt')

# 将字符串写入文件

f = open('somefile.txt', 'w')
f.write(u'你好。Hello, ')
f.write(u'World!')
f.close()

# 从文件读取字符串

#f = open('somefile.txt', 'r')
f = open('somefile.txt')
print(f.read())
f.close()

运行结果如下图所示,不会抛出 UnicodeEncodeError 异常了:

在 Python 2.7.17 中显式地往文件中写入 Unicode 字符串
在 Python 2.7.17 中显式地往文件中写入 Unicode 字符串

Ricky 在代码中输出( print )了默认的字符编码格式( sys.getdefaultencoding() ),我们可以看到默认的字符编码格式已经被修改为 UTF-8 了。

当然了,这种方法是不推荐的,具体要怎么解决这个问题呢?别着急,文章后面会有说明。

sys.setdefaultencoding(‘utf-8’) 导致的问题

除了 reload(sys) 导致的问题以外,sys.setdefaultencoding(‘utf-8’) 也会导致一些问题。简单来说这么做将会使得一些代码的行为变得怪异,而这怪异还不好修复,以一个不可见的 bug 存在着。下面我们举两个例子。

1 、编码错误

我们来看看这份代码:

def print_string(string):
    try:
        print u"%s" % string
    except UnicodeError:
        print 'UnicodeError_START'
        print u"%s" % unicode(string, encoding='latin-1')
        print 'UnicodeError_END'

print "\n==== before sys.setdefaultencoding('utf-8') ====\n"
print_string(u"þ".encode("latin-1"))

import sys
stdi,stdo,stde = sys.stdin,sys.stdout,sys.stderr
reload(sys)
sys.stdin,sys.stdout,sys.stderr = stdi,stdo,stde
sys.setdefaultencoding('utf-8')

print "\n==== after sys.setdefaultencoding('utf-8') ====\n"
print_string(u"þ".encode("latin-1"))

运行结果如下图所示:

编码错误
编码错误

你会发现同样一份代码,在 sys.setdefaultencoding(‘utf-8’) 前后,输出的结果是不一样的:

  • sys.setdefaultencoding(‘utf-8’) 之前输出:þ
  • sys.setdefaultencoding(‘utf-8’) 之后输出:þ

我们先来讨论一下 sys.setdefaultencoding(‘utf-8′) 之前的情况:þ 的 latin-1 编码的十六进制表示是 C3 BE ,显然是超出了只有 128 个字符的 ASCII 编码集,所以在 print(u”%s” % string) 时引发 UnicodeError 异常,进入异常处理。在异常处理的这句中 unicode(string, encoding=’latin-1’) ,会显式使用 latin-1 编码进行解码,此时就能正确打印出字符 þ 。

当我们将 defaultencoding 设置为 utf-8 之后,因为 utf-8 的字符范围完全覆盖 latin-1 ,因此会直接使用 utf-8 进行解码。C3 BE 在 utf-8 中是 þ ,于是我们打印出了完全不同的字符。

可能你会觉得我不会写这样的代码,如果我写了也会做修正。但如果是第三方库这么写了呢?项目依赖的第三方库就这么 bug 了。如果你不依赖第三方库,那么下面这个 bug ,还是逃不过。

2 、dictionray(字典)行为异常

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

  1. 一种是使用 dictionary(字典)的 in 操作符
  2. 另一种是使用 == 操作符

我们先来看看代码:

d = {1 : 2, '1' : '2', '你好' : 'hello'}

def key_in_dict(key):
    if key in d:
        return True
    return False

def key_in_dict_2(key):
    for k in d:
        if k == key:
            return True
    return False

print "\n==== before sys.setdefaultencoding('utf-8') ====\n"
print key_in_dict('你好')
print key_in_dict_2('你好')
print key_in_dict(u'你好')
print key_in_dict_2(u'你好')

import sys
stdi,stdo,stde = sys.stdin,sys.stdout,sys.stderr
reload(sys)
sys.stdin,sys.stdout,sys.stderr = stdi,stdo,stde
sys.setdefaultencoding('utf-8')

print "\n==== after sys.setdefaultencoding('utf-8') ====\n"
print key_in_dict('你好')
print key_in_dict_2('你好')
print key_in_dict(u'你好')
print key_in_dict_2(u'你好')

运行结果如下图所示:

dictionray(字典)行为异常
dictionray(字典)行为异常

我们能够直观地看到,在 sys.setdefaultencoding(‘utf-8’) 前后,输出的结果是不一样的,甚至还报了一个 Warning(警告)。

我们先来看看 key_in_dict 函数,也就是使用 dictionary(字典)的 in 操作符:

  • sys.setdefaultencoding(‘utf-8’) 之前,key_in_dict(‘你好’) 输出:True
  • sys.setdefaultencoding(‘utf-8’) 之前,key_in_dict(u‘你好’) 输出:False
  • sys.setdefaultencoding(‘utf-8’) 之后,key_in_dict(‘你好’) 输出:True
  • sys.setdefaultencoding(‘utf-8’) 之后,key_in_dict(u‘你好’) 输出:False

dictionary(字典)的 in 操作符会先对 “ if key in d ” 中的 key 做哈希,然后再用 key 的哈希值跟字典 d 中各个键的哈希值进行比较,从而判断 key 是否存在于字典中。

虽然看上去都是两个中文字符 “ 你好 ” ,但是因为编码方式不同,所以 ‘你好’ 这种 byte string(即 str 类型的字符串)和 u’你好’ 这种采用 Unicode 字符编码的 text string(即 Unicode 类型的字符串)在计算机底层其实是不一样的。我们来看一个例子,在执行 sys.setdefaultencoding(‘utf-8’) 之前和之后,再分别执行以下两行 print 语句:

d = {1 : 2, '1' : '2', '你好' : 'hello'}

def key_in_dict(key):
    if key in d:
        return True
    return False

print "\n==== before sys.setdefaultencoding('utf-8') ====\n"
print key_in_dict('你好')
print key_in_dict(u'你好')
print ''
print repr('你好')
print repr(u'你好')

import sys
stdi,stdo,stde = sys.stdin,sys.stdout,sys.stderr
reload(sys)
sys.stdin,sys.stdout,sys.stderr = stdi,stdo,stde
sys.setdefaultencoding('utf-8')

print "\n==== after sys.setdefaultencoding('utf-8') ====\n"
print key_in_dict('你好')
print key_in_dict(u'你好')
print ''
print repr('你好')
print repr(u'你好')

运行结果如下所示:

str 类型和 unicode 类型
str 类型和 unicode 类型

汇总起来就是:

  • sys.setdefaultencoding(‘utf-8’) 之前,print repr(‘你好’) 输出:’\xe4\xbd\xa0\xe5\xa5\xbd’
  • sys.setdefaultencoding(‘utf-8’) 之前,print repr(u‘你好’) 输出:u’\u4f60\u597d’
  • sys.setdefaultencoding(‘utf-8’) 之后,print repr(‘你好’) 输出:’\xe4\xbd\xa0\xe5\xa5\xbd’
  • sys.setdefaultencoding(‘utf-8’) 之后,print repr(u‘你好’) 输出:u’\u4f60\u597d’

也就是说,无论是否更改默认的字符编码,’你好’ 这个 byte string 在计算机底层均表示为 ‘\xe4\xbd\xa0\xe5\xa5\xbd’ ,u’你好’ 这个 text string 在计算机底层均表示为 u’\u4f60\u597d’ 。

那么 ‘\xe4\xbd\xa0\xe5\xa5\xbd’ 和 u’\u4f60\u597d’ 在分别做哈希以后,得到的两个哈希值自然也是不一样的。换句话说,如果现在有两个 ‘你好’( byte string ),它们在计算机底层均表示为 ‘\xe4\xbd\xa0\xe5\xa5\xbd’ ,那么它们的哈希值就是一样的;同理,如果现在有两个 u’你好’( text string ),它们在计算机底层均表示为 u’\u4f60\u597d’ ,那么它们的哈希值也是一样的。

综上所述,字典 d = {1 : 2, ‘1’ : ‘2’, ‘你好’ : ‘hello’} 中的 ‘你好’ 显然是 byte string ,那么:

  • 在运行到 key_in_dict(‘你好’) 这行代码时,因为函数的参数 ‘你好’ 也是 byte string ,所以该函数会返回 True ;
  • 在运行到 key_in_dict(u’你好’) 这行代码时,因为函数的参数 u’你好’ 是 text string ,所以该函数会返回 False 。

思考一:

如果我们将字典 d 中的 ‘你好’ 改成 u’你好’ ,也就是 d = {1 : 2, ‘1’ : ‘2’, u‘你好’ : ‘hello’} ,那么运行结果就会变成:

  • sys.setdefaultencoding(‘utf-8’) 之前,key_in_dict(‘你好’) 输出:False
  • sys.setdefaultencoding(‘utf-8’) 之前,key_in_dict(u‘你好’) 输出:True
  • sys.setdefaultencoding(‘utf-8’) 之后,key_in_dict(‘你好’) 输出:False
  • sys.setdefaultencoding(‘utf-8’) 之后,key_in_dict(u‘你好’) 输出:True

因为字典 d = {1 : 2, ‘1’ : ‘2’, u‘你好’ : ‘hello’} 中的 u’你好’ 显然是 text string ,那么:

  • 在运行到 key_in_dict(‘你好’) 这行代码时,因为函数的参数 ‘你好’ 是 byte string ,所以该函数会返回 False ;
  • 在运行到 key_in_dict(u’你好’) 这行代码时,因为函数的参数 u’你好’ 也是 text string ,所以该函数会返回 True 。

这里给出代码和运行结果:

d = {1 : 2, '1' : '2', u'你好' : 'hello'}

def key_in_dict(key):
    if key in d:
        return True
    return False

print "\n==== before sys.setdefaultencoding('utf-8') ====\n"
print key_in_dict('你好')
print key_in_dict(u'你好')

import sys
stdi,stdo,stde = sys.stdin,sys.stdout,sys.stderr
reload(sys)
sys.stdin,sys.stdout,sys.stderr = stdi,stdo,stde
sys.setdefaultencoding('utf-8')

print "\n==== after sys.setdefaultencoding('utf-8') ====\n"
print key_in_dict('你好')
print key_in_dict(u'你好')
思考一
思考一

思考二:

如果我们现在想查找 ‘1’ 这个键是否存在于字典 d = {1 : 2, ‘1’ : ‘2’, ‘你好’ : ‘hello’} 中,那么运行结果是:

  • sys.setdefaultencoding(‘utf-8’) 之前,key_in_dict(‘1’) 输出:True
  • sys.setdefaultencoding(‘utf-8’) 之前,key_in_dict(u‘1’) 输出:True
  • sys.setdefaultencoding(‘utf-8’) 之后,key_in_dict(‘1’) 输出:True
  • sys.setdefaultencoding(‘utf-8’) 之后,key_in_dict(u‘1’) 输出:True

注意,我们查找的是字符串 ‘1’ ,而非数字 1 。

为啥返回的结果全部是 True 呢?相信你应该已经猜到了,从计算机底层的角度来看,’1’( byte string )和 u’1’( text string )的编码其实是一样的:

  • sys.setdefaultencoding(‘utf-8’) 之前,print repr(‘1′) 输出:’1’
  • sys.setdefaultencoding(‘utf-8’) 之前,print repr(u‘1’) 输出:u’1′
  • sys.setdefaultencoding(‘utf-8’) 之后,print repr(‘1′) 输出:’1’
  • sys.setdefaultencoding(‘utf-8’) 之后,print repr(u‘1’) 输出:u’1′

这里给出代码和运行结果:

d = {1 : 2, '1' : '2', '你好' : 'hello'}

def key_in_dict(key):
    if key in d:
        return True
    return False

print "\n==== before sys.setdefaultencoding('utf-8') ====\n"
print key_in_dict('1')
print key_in_dict(u'1')
print ''
print repr('1')
print repr(u'1')

import sys
stdi,stdo,stde = sys.stdin,sys.stdout,sys.stderr
reload(sys)
sys.stdin,sys.stdout,sys.stderr = stdi,stdo,stde
sys.setdefaultencoding('utf-8')

print "\n==== after sys.setdefaultencoding('utf-8') ====\n"
print key_in_dict('1')
print key_in_dict(u'1')
print ''
print repr('1')
print repr(u'1')
思考二
思考二

思考三:

综上所述,在 Python 2 中,虽然同样是两个中文字符 “ 你好 ” ,但是因为计算机底层编码不一样,导致计算机认为 ‘你好’( byte string )和 u’你好’( text string )这两个字符串根本就不一样。

那在 Python 3 中又是什么情况呢?上文曾经提到,’你好’ 和 u’你好’ 在 Python 3 看来都是一回事儿了:

  • 在 Python 3 中,print(repr(‘你好’)) 输出:’你好’
  • 在 Python 3 中,print(repr(u‘你好’)) 输出:’你好’

所以如果在 Python 3 中执行 key_in_dict 函数,那么返回结果都是 True :

  • 在 Python 3 中,print(key_in_dict(‘你好’)) 输出:True
  • 在 Python 3 中,print(key_in_dict(u‘你好’)) 输出:True

也就是说 Python 3 更符合人类的思维,无需像 Python 2 那样还要考虑计算机底层编码不一样的问题。

这里给出代码和运行结果:

d = {1 : 2, '1' : '2', '你好' : 'hello'}

def key_in_dict(key):
    if key in d:
        return True
    return False

print(key_in_dict('你好'))
print(key_in_dict(u'你好'))
print('')
print(repr('你好'))
print(repr(u'你好'))

# Py 3 默认的字符编码格式就是 utf-8 ,无需再 sys.setdefaultencoding('utf-8')
print('')
import sys
print(sys.getdefaultencoding())
思考三
思考三

我们再来看看 key_in_dict_2 函数,也就是使用 == 操作符:

  • sys.setdefaultencoding(‘utf-8’) 之前,key_in_dict_2(‘你好’) 输出:True
  • sys.setdefaultencoding(‘utf-8’) 之前,key_in_dict_2(u‘你好’) 输出:False ,且报 UnicodeWarning 警告
  • sys.setdefaultencoding(‘utf-8’) 之后,key_in_dict_2(‘你好’) 输出:True
  • sys.setdefaultencoding(‘utf-8’) 之后,key_in_dict_2(u‘你好’) 输出:True

我们先来看看 encode(编码)和 decode(解码)两个字符串方法。所谓的 encode() 方法就是以指定的编码格式编码字符串,decode() 方法就是以指定的编码格式解码字符串。这么说起来可能难以理解,我们举个例子看看:

s = 'ABC'

# 使用 base64 字符编码,将 s 编码为 en_s
en_s = s.encode('base64')
# 打印 en_s 的内容
print 'en_s :', repr(en_s)

# 使用 base64 字符编码,将 en_s 解码为 de_s
de_s = en_s.decode('base64')
# 打印 de_s 的内容
print 'de_s :', repr(de_s)

# s 、en_s 和 de_s 均是 str 类型
print ''
print 's :', s.__class__
print 'en_s :', en_s.__class__
print 'de_s :', de_s.__class__

运行结果如下图所示:

encode 和 decode
encode 和 decode

我们可以看到,将 str 类型的字符串编码成采用 base64 字符编码的 str 类型的字符串,需要调用 s.encode(‘base64’) 方法;若想解码回来,就需要调用 en_s.decode(‘base64’) 方法。这里需要注意的是,字符串变量 s 、en_s 和 de_s 均是 str 类型的字符串。

base64 编解码:

encode : str(utf-8) → str(base64)
decode : str(base64) → str(utf-8)

看到这里你一定会这么想:将 str 类型的字符串转换成 Unicode 类型的字符串,需要调用 encode() 编码方法;若想转换回来,就需要调用 decode() 解码方法。其实呢,这是错误的。

正确的方式应该是:将 str 类型的字符串转换成 Unicode 类型的字符串,需要调用 decode() 解码方法;若想转换回来,就需要调用 encode() 编码方法。

Unicode 编解码:

encode : unicode → str(utf-8)
decode : str(utf-8) → unicode

为什么 str 类型的字符串要通过 “ 解码 ” 才能转换为 Unicode 类型的字符串呢?因为在 Python 2 中 Unicode 类型的字符串的层级要比 str 类型的字符串低:

Unicode 和 base64 编解码:

encode : unicode → str(utf-8) → str(base64)
decode : str(base64) → str(utf-8) → unicode

我们来看一个例子:

u = u'你好,世界!'
print 'u :', repr(u)

s = u.encode('utf-8')  # unicode → str(utf-8)
print 's :', repr(s)

en_s = s.encode('base64')  # str(utf-8) → str(base64)
print 'en_s :', repr(en_s)

s = en_s.decode('base64')  # str(base64) → str(utf-8)
print 's :', repr(s)

u = s.decode('utf-8')  # str(utf-8) → unicode
print 'u :', repr(u)

运行结果如下图所示:

encode 和 decode
encode 和 decode

那么 == 操作符又是如何判断两变量是否相等的呢?当 Python 看到 == 操作符一边是 byte string(字节字符串,str 类型),另一边是 text string(文本字符串,Unicode 类型)的时候,它会将 byte string 解码( decode )为采用 Unicode 字符编码的 text string ,然后再执行 == 操作符的比较操作。

我们先来解释一下程序为什么会报 UnicodeWarning 警告:

Warning (from warnings module):
  File "/Users/huangyuji/Documents/Python/11/temp.py", line 10
    if k == key:
UnicodeWarning: Unicode equal comparison failed to convert both arguments to Unicode - interpreting them as being unequal

Unicode 警告:在 Unicode 相等比较中,无法将两个参数转换为 Unicode -(于是)将它们解释为不相等。

在将 byte string 解码( decode )为采用 Unicode 字符编码的 text string 的时候,Python 2 需要什么字符编码来对 byte string 进行解码呢?因为 Python 2 默认的字符编码是 ASCII ,所以此时 Python 2 尝试使用 ASCII 字符编码来对 byte string 进行解码(如执行 str.decode(‘ascii’) 方法)。同样,如果 byte string 中包含中文字符,那么尝试使用 ASCII 字符编码来对 byte string 进行解码的操作就会出错,这就是为什么会引发 UnicodeWarning 警告的原因。

因为解码时出错(此时引发 UnicodeWarning 警告),所以 == 操作符会认定两个字符串是不一样的,于是 == 操作符返回 False(注意,这是警告,不是异常,警告在默认情况下是不会导致程序停止运行的,程序还会继续运行)。

综上所述,这就是为什么在执行 sys.setdefaultencoding(‘utf-8′) 之前,key_in_dict_2(u’你好’) 输出 False 的原因 —— 虽然 key_in_dict_2 函数的参数是 u’你好’( text string ),但是字典 d 中的 ’你好’ 是 byte string ,所以在对 ’你好’ 进行转码时出错。

在执行 sys.setdefaultencoding(‘utf-8′) 之后,key_in_dict_2(u’你好’) 输出 True 是因为此时 Python 2 默认的字符编码已经被修改为 UTF-8 ,所以此时 Python 2 尝试使用 UTF-8 字符编码来对 byte string 进行解码(如执行 str.decode(‘utf-8’) 方法),自然是可以解码成功的。

而另外两种情况,key_in_dict_2 函数的参数 ’你好’ 和字典 d 中的 ’你好’ 均是 byte string ,所以不需要执行解码( decode )操作。

总而言之,在 sys.setdefaultencoding(‘utf-8’) 之后,in 和 == 的行为会不一致。

问题的根源:Python2 中的字符串

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

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

Python 还会在必要的情况下,尝试对字节( byte string )做自动类型转换,例如在上文中提到的 == ,或者在对字节( byte string )和文本( text string )做拼接时。如果不知道字节( byte string )属于什么字符编码( encoding ),那么两个不同类型之间的转换是无法进行的,于是 Python 需要一个默认的字符编码,Python 2 选择了 ASCII 。然而,众所周知,在需要转换的场景,ASCII 没啥用(仅包含 128 个字符,够什么吃)。

在历经这么多年吐槽后,Python 3 的默认字符编码终于是 UTF-8 ,这省去了很多麻烦。

最佳实践

说了这么多,那在不迁移到 Python 3 的情况下该怎么做呢?

  • 所有 text string 都应该是 Unicode 类型而不是 str 类型。如果你在操作 text ,而类型却是 str ,那就是在制造 bug 。
  • 在需要转换的时候显式转换。从 byte string 转换成 text string 用 s.decode(encoding) ,从 text string 转换成 byte string 用 u.encode(encoding) 。
  • 从外部读取数据时,默认它是 byte string ,然后 decode 成需要的 text string ;同样的,当需要向外部发送 text string 时,encode 成 byte string 再发送。

以本文最开头的那个例子为例(将字符串 “ 你好。Hello, World! ” 存入 somefile.txt 中),我们看看 Python 2 中该怎么做,以及同一份代码如何既可以在 Python 2 中运行,又可以在 Python 3 中运行:

from sys import version_info

def encode_utf8(string):
    if version_info.major == 2:
        return string.encode('utf-8')
    elif version_info.major == 3:
        return string

def decode_utf8(string):
    if version_info.major == 2:
        return string.decode('utf-8')
    elif version_info.major == 3:
        return string

# 删除文件
import os
if os.path.exists('somefile.txt'):
    os.remove('somefile.txt')

# 将字符串写入文件

f = open('somefile.txt', 'w')
f.write(encode_utf8(u'你好。Hello, '))
f.write(encode_utf8(u'World!'))
f.close()

# 从文件读取字符串

#f = open('somefile.txt', 'r')
f = open('somefile.txt')
print(decode_utf8(f.read()))
f.close()

运行结果如下图所示:

Python 2 最佳实践
Python 2 最佳实践
Python 3 最佳实践
Python 3 最佳实践

One more thing

Ricky 发现在文件对象的 read 方法中,read 方法的参数在 Python 2 和 Python 3 上还不太一样。在 Python 2 中,read 方法的参数是字节数(比如在 UTF-8 编码下,1 个中文字符有 3 个字节);而在 Python 3 中,read 方法的参数是字符数( 1 个中文字符就是 1 个字符)。

其实这倒也不奇怪,因为 Python 2 是不区分字节和字符的。演示代码如下所示:

# -*- coding: utf-8 -*-
from sys import version_info

def encode_utf8(string):
    if version_info.major == 2:
        return string.encode('utf-8')
    elif version_info.major == 3:
        return string

def decode_utf8(string):
    if version_info.major == 2:
        return string.decode('utf-8')
    elif version_info.major == 3:
        return string

# 删除文件
import os
if os.path.exists('somefile.txt'):
    os.remove('somefile.txt')

# 将字符串写入文件

f = open('somefile.txt', 'w')
f.write(encode_utf8(u'你好。Hello, '))
f.write(encode_utf8(u'World!'))
f.close()

# 从文件读取字符串

#f = open('somefile.txt', 'r')
f = open('somefile.txt')

if version_info.major == 2:
    print(decode_utf8(f.read(6)))  # Py 2 中这里的参数是【字节数】,2 个【汉字】在 UTF-8 中是 6 个【字节】
    print(decode_utf8(f.read(3)))  # 1 个【中文标点符号】在 UTF-8 中也是 3 个【字节】
    print(decode_utf8(f.read(1)))  # 1 个【英文字母】在 UTF-8 中是 1 个【字节】
elif version_info.major == 3:
    print(decode_utf8(f.read(2)))  # Py 3 中这里的参数是【字符数】,2 个【汉字】是 2 个【字符】
    print(decode_utf8(f.read(1)))  # 1 个【中文标点符号】也是 1 个【字符】
    print(decode_utf8(f.read(1)))  # 1 个【英文字母】也是 1 个【字符】

print(decode_utf8(f.read()))  # 打印剩余字符
f.close()

运行结果如下图所示:

在 Python 2 中,关于 read 方法的参数
在 Python 2 中,关于 read 方法的参数
在 Python 3 中,关于 read 方法的参数
在 Python 3 中,关于 read 方法的参数

开头这一行 # -*- coding: utf-8 -*- 表明上面的 Python 代码由 UTF-8 编码。是的,只是代码的字符编码,不能通过这个值来设置默认的字符编码。

 

参考自:

  • https://baike.baidu.com/item/Unicode
  • https://blog.csdn.net/qq_36761831/article/details/82291166
  • https://www.cnblogs.com/catkins/p/5270411.html
  • https://blog.csdn.net/nero_g/article/details/55253356
  • https://blog.csdn.net/weixin_34195364/article/details/88988634
  • https://blog.ernest.me/post/python-setdefaultencoding-unicode-bytes
  • http://blog.notdot.net/2010/07/Getting-unicode-right-in-Python
  • https://anonbadger.wordpress.com/2015/06/16/why-sys-setdefaultencoding-will-break-code/
  • https://www.iteye.com/blog/in355hz-1860787

Was this article helpful?

Related Articles

1 Comment

  1. 这里补充一个知识点,该知识点跟本篇文章是没有关系的:即在 Python 3.8.1 中,str(utf-8) → byte(utf-8) 要怎么做呢?或者说如何将 class str 转为 class byte( 即 str 类型转为 byte 类型 )?

    假设现在有一个 line 变量,line = ‘Welcome’ ,该变量是采用 UTF-8 编码的 str 类型的变量,只需 line = line.encode(‘utf-8’) 即可将 line 变量转换为采用 UTF-8 编码的 byte 类型的变量。

    即 encode : str(utf-8) → byte(utf-8) 。

    asynchat 模块中的 async_chat.push(data) 方法的 data 参数,就需要是 byte 类型的。

Leave A Comment?

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据