异常消息的默认编码
以下代码用来检查当输入一个非ASCII符号时,float()
方法的表现:
import sys
try:
float(u'\xbd')
except ValueError as e:
print sys.getdefaultencoding() # in my system, this is 'ascii'
print e[0].decode('latin-1') # u'invalid literal for float(): ' followed by the 1/2 (one half) character
print unicode(e[0]) # raises "UnicodeDecodeError: 'ascii' codec can't decode byte 0xbd in position 29: ordinal not in range(128)"
我想问的是:为什么错误信息e[0]
是用Latin-1编码的呢?默认的编码是ASCII,而这似乎是unicode()
所期待的。
平台是Ubuntu 9.04,Python 2.6.2
4 个回答
ASCII编码只包含值小于等于127的字节。这些字节所代表的字符范围在大多数编码中是一样的;换句话说,在ASCII、latin-1、UTF-8等编码中,“A”的值都是chr(65)
。
但是,"一半"这个符号不在ASCII字符集中,所以当Python试图把这个符号编码成ASCII时,它只能失败。
更新:下面是发生了什么(我假设我们在讨论CPython):
float(u'\xbd')
会调用PyFloat_FromString
,这个函数在floatobject.c文件中。这个函数接收一个unicode对象,然后又调用了在unicodeobject.c中的PyUnicode_EncodeDecimal
。从我浏览的代码来看,这个函数会把unicode对象转换成字符串,它的做法是把每个unicode代码点小于256的字符替换成对应的字节,也就是说,"一半"这个字符的代码点是189,所以它会被转换成chr(89)
。
然后,PyFloat_FromString
像往常一样工作。这时,它处理的是一个普通字符串,而这个字符串恰好包含了一个非ASCII范围的字节。它对此并不在意;它只是在找一个不是数字、不是小数点之类的字节,所以就抛出了值错误。
这个异常的参数是一个字符串
"invalid literal for float(): " + evil_string
这没问题;毕竟,异常消息就是一个字符串。只有当你尝试用默认的ASCII编码来解码这个字符串时,问题才会出现。
非常好的问题!
我主动去查了一下Python的源代码,这在设置好的Linux系统上只需要一个命令就能做到(apt-get source python2.5
)。
真是的,John Millikin比我先找到了答案。没错,PyUnicode_EncodeDecimal
就是答案,它在这里执行这个操作:
/* (Loop ch in the unicode string) */
if (Py_UNICODE_ISSPACE(ch)) {
*output++ = ' ';
++p;
continue;
}
decimal = Py_UNICODE_TODECIMAL(ch);
if (decimal >= 0) {
*output++ = '0' + decimal;
++p;
continue;
}
if (0 < ch && ch < 256) {
*output++ = (char)ch;
++p;
continue;
}
/* All other characters are considered unencodable */
collstart = p;
collend = p+1;
while (collend < end) {
if ((0 < *collend && *collend < 256) ||
!Py_UNICODE_ISSPACE(*collend) ||
Py_UNICODE_TODECIMAL(*collend))
break;
}
你看,它保留了所有小于256的Unicode字符,这些字符就是latin-1字符,这是因为Unicode要向后兼容。
补充说明
有了这个,你可以通过尝试其他非latin-1字符来验证,这样会抛出不同的异常:
>>> float(u"ħ")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'decimal' codec can't encode character u'\u0127' in position 0: invalid decimal Unicode string
e[0] 不是用 latin-1 编码的;只是恰好字节 \xbd 在用 latin-1 解码时,变成了字符 U+00BD。
这个转换发生在 Objects/floatobject.c
文件里。
首先,unicode 字符串必须转换成字节字符串。这是通过 PyUnicode_EncodeDecimal()
来完成的:
if (PyUnicode_EncodeDecimal(PyUnicode_AS_UNICODE(v),
PyUnicode_GET_SIZE(v),
s_buffer,
NULL))
return NULL;
这个函数是在 unicodeobject.c
文件中实现的。它并不进行任何字符集的转换,而是直接写入与字符串的 unicode 编码值相等的字节。在这个例子中,U+00BD 转换成了 0xBD。
格式化错误信息的语句是:
PyOS_snprintf(buffer, sizeof(buffer),
"invalid literal for float(): %.200s", s);
这里 s
包含了之前创建的字节字符串。PyOS_snprintf()
写入的是一个字节字符串,而 s
也是一个字节字符串,所以它直接把它包含进去了。