今天帮同事研究一个莫名其妙的unicodedecodeerror时发现了python字符串格式化中的一个小陷阱,在此记录一下。原本的代码过于复杂,有太多与问题无关的东西,所以我在ipython里简单试验复现了问题,过程如下:
in [4]: a = ‘你好世界’
in [5]: print ‘say this: %s’ % a
say this: 你好世界
in [6]: print ‘say this: %s and say that: %s’ % (a, ‘hello world’)
say this: 你好世界 and say that: hello world
in [7]: print ‘say this: %s and say that: %s’ % (a, u’hello world’)
—————————————————————————
unicodedecodeerror traceback (most recent call last)
/home/jerry/ in ()
unicodedecodeerror: ‘ascii’ codec can’t decode byte 0xe4 in position 10: ordinal not in range(128)
in [8]: a
out[8]: ‘\xe4\xbd\xa0\xe5\xa5\xbd\xe4\xb8\x96\xe7\x95\x8c’
看到in [7]之后的那个很怪异的unicodedecodeerror没?它和上一句的唯一区别是’hello world‘变成了一个unicode对象而非str对象。但问题是,’hello world’只是单纯的英文字符串,不包含任何ascii之外的字符,怎么会无法decode呢?再仔细看看异常附带的message,里面提到了0xe4,这个显然不是’hello world‘里面的,所以只能怀疑那句中文了,in [8]把它的字节序列打印了出来,果然就是它,第一个就是0xe4。
看来在字符串格式化的时候python试图将a decode成unicode对象,并且decode时用的还是默认的ascii编码而非实际的utf-8编码。那这又是怎么回事呢??下面继续我们的试验:
in [9]: ‘say this: %s’ % ‘hello’
out[9]: ‘say this: hello’
in [10]: ‘say this: %s’ % u’hello’
out[10]: u’say this: hello’
仔细看,in [9]中的’hello’是普通的字符串,结果也是字符串(str对象),而in [10]中的’hello’变成了unicode对象,格式化的结果也变成unicode了(注意结果开头的那个u)。
所以真相是这样的:python在格式化字符串的时候有一些隐藏着的小动作:如果%s对应的参数里有unicode,那么最终的结果也是unicode。在这种情况下模版字符串以及所有的%s参数中的str都会被decode成unicode,然而这个decode是隐式的,用户无法指定其使用的charset,python只能用默认的ascii。如果正好里面有非ascii编码的字符串,就完蛋了……
看看python文档怎么说的:
if format is a unicode object, or if any of the objects being converted using the %s conversion are unicode objects, the result will also be a unicode object.
如果代码里混合着str和unicode,这种问题很容易出现。在同事的代码里,中文字符串是用户输入的,经过了正确的编码处理,是以utf-8编码的str对象;但那个惹事的unicode对象,虽然其内容都是ascii码,但其来源是sqlite3数据库查询的结果,而sqlite的api返回的字符串都是unicode对象,所以导致了这么怪异的结果。
python 2的str和unicode真的挺坑爹,已经被它们害过好几次了。python 3在这方面很有提升,期待它的全面普及!