异常消息的默认编码

6 投票
4 回答
4150 浏览
提问于 2025-04-15 14:01

以下代码用来检查当输入一个非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 个回答

2

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编码来解码这个字符串时,问题才会出现。

5

非常好的问题!

我主动去查了一下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
9

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 也是一个字节字符串,所以它直接把它包含进去了。

撰写回答