Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 293 – 编解码器错误处理回调

作者:
Walter Dörwald <walter at livinglogic.de>
状态:
最终版
类型:
标准跟踪
创建日期:
2002年6月18日
Python 版本:
2.3
发布历史:
2002年6月19日

目录

摘要

本PEP旨在通过一种更灵活的基于回调的方法来扩展Python固定的编解码器错误处理方案。

Python目前对编解码器错误处理器使用固定的错误处理。本PEP描述了一种机制,允许Python使用函数回调作为错误处理器。通过这些更灵活的错误处理器,可以为现有编解码器添加新功能,例如提供回退解决方案或在标准编解码器映射不适用时提供不同的编码。

规范

目前,编解码器错误处理算法集固定为“strict”、“replace”或“ignore”,并且这些算法的语义为每个编解码器单独实现。

提议的补丁将通过一个编解码器错误处理程序注册表使错误处理算法集可扩展,该注册表将处理程序名称映射到处理程序函数。该注册表由以下两个C函数组成

int PyCodec_RegisterError(const char *name, PyObject *error)

PyObject *PyCodec_LookupError(const char *name)

及其Python对应项

codecs.register_error(name, error)

codecs.lookup_error(name)

PyCodec_LookupError 如果在此名称下没有注册回调函数,则会引发 LookupError

与编码名称注册表类似,无法取消注册回调函数或遍历可用函数。

编解码器将按以下方式使用回调函数:当编解码器遇到编码/解码错误时,按名称查找回调函数,错误信息存储在异常对象中,并使用此对象调用回调。回调返回有关如何继续的信息(或引发异常)。

对于编码,异常对象将如下所示

class UnicodeEncodeError(UnicodeError):
    def __init__(self, encoding, object, start, end, reason):
        UnicodeError.__init__(self,
            "encoding '%s' can't encode characters " +
            "in positions %d-%d: %s" % (encoding,
                start, end-1, reason))
        self.encoding = encoding
        self.object = object
        self.start = start
        self.end = end
        self.reason = reason

此类型将在C中实现,并具有适当的属性设置器和获取器方法,这些属性具有以下含义

  • encoding:编码的名称;
  • object:为其调用 encode() 的原始unicode对象;
  • start:第一个无法编码字符的位置;
  • end:(最后一个无法编码字符的位置)+1(或者如果从start到对象末尾的所有字符都无法编码,则为对象的长度);
  • reasonobject[start:end] 无法编码的原因。

如果对象有连续的无法编码字符,并且这些字符无法编码的原因相同,则编码器应将这些字符收集起来,一次性调用回调。编码器不要求实现此行为,但可以为每个字符调用回调,但强烈建议实现收集方法。

回调不得修改异常对象。如果回调不引发异常(无论是传入的还是不同的异常),它必须返回一个元组

(replacement, newpos)

replacement是一个unicode对象,编码器将对其进行编码并发出,而不是无法编码的 object[start:end] 部分;newpos指定对象中的一个新位置,编码器将在此处(编码replacement后)继续编码。

newpos的负值被视为相对于对象末尾。如果newpos超出范围,编码器将引发 IndexError

如果替换字符串本身包含无法编码的字符,编码器将引发异常对象(但在引发之前可能会设置不同的原因字符串)。

如果发生进一步的编码错误,编码器可以重用异常对象用于下一次回调调用。此外,编码器可以缓存 codecs.lookup_error 的结果。

如果回调不知道如何处理异常,它必须引发 TypeError

解码与编码类似,但有以下区别

  • 异常类名为 UnicodeDecodeError,属性对象是解码器当前正在解码的原始8位字符串。
  • 解码器将用构成一个无法解码序列的字节调用回调,即使在第一个无法解码序列之后紧接着有多个无法解码的序列,并且它们无法解码的原因相同。例如,对于“unicode-escape”编码,当解码非法字符串 \\u00\\u01x 时,回调将被调用两次(一次用于 \\u00,一次用于 \\u01)。这样做是为了能够生成正确数量的替换字符。
  • 从回调返回的replacement是一个unicode对象,解码器将按原样发出,无需进一步处理,而不是无法解码的 object[start:end] 部分。

还有第三个API使用旧的strict/ignore/replace错误处理方案

PyUnicode_TranslateCharmap/unicode.translate

提议的补丁将增强 PyUnicode_TranslateCharmap,使其也支持回调注册表。这有一个额外的副作用,即 PyUnicode_TranslateCharmap 将支持多字符替换字符串(参见SF功能请求#403100 [1])。

对于 PyUnicode_TranslateCharmap,异常类将命名为 UnicodeTranslateErrorPyUnicode_TranslateCharmap 将收集所有连续的不可转换字符(即那些映射到 None 的字符),并用它们调用回调。从回调返回的替换是一个unicode对象,它将按原样放入翻译结果中,无需进一步处理。

所有编码器和解码器都允许自行实现回调功能,如果它们识别回调名称(即,如果它是像“strict”、“replace”和“ignore”这样的系统回调)。提议的补丁将添加两个额外的系统回调名称:“backslashreplace”和“xmlcharrefreplace”,它们可用于编码和翻译,并且也将为所有编码器和 PyUnicode_TranslateCharmap 在适当位置实现。

这五个回调的Python等价物将如下所示

def strict(exc):
    raise exc

def ignore(exc):
    if isinstance(exc, UnicodeError):
        return (u"", exc.end)
    else:
        raise TypeError("can't handle %s" % exc.__name__)

def replace(exc):
     if isinstance(exc, UnicodeEncodeError):
         return ((exc.end-exc.start)*u"?", exc.end)
     elif isinstance(exc, UnicodeDecodeError):
         return (u"\\ufffd", exc.end)
     elif isinstance(exc, UnicodeTranslateError):
         return ((exc.end-exc.start)*u"\\ufffd", exc.end)
     else:
         raise TypeError("can't handle %s" % exc.__name__)

def backslashreplace(exc):
     if isinstance(exc,
         (UnicodeEncodeError, UnicodeTranslateError)):
         s = u""
         for c in exc.object[exc.start:exc.end]:
            if ord(c)<=0xff:
                s += u"\\x%02x" % ord(c)
            elif ord(c)<=0xffff:
                s += u"\\u%04x" % ord(c)
            else:
                s += u"\\U%08x" % ord(c)
         return (s, exc.end)
     else:
         raise TypeError("can't handle %s" % exc.__name__)

def xmlcharrefreplace(exc):
     if isinstance(exc,
         (UnicodeEncodeError, UnicodeTranslateError)):
         s = u""
         for c in exc.object[exc.start:exc.end]:
            s += u"&#%d;" % ord(c)
         return (s, exc.end)
     else:
         raise TypeError("can't handle %s" % exc.__name__)

这五个回调处理器也将作为 codecs.strict_errorcodecs.ignore_errorcodecs.replace_errorcodecs.backslashreplace_errorcodecs.xmlcharrefreplace_error 供Python访问。

基本原理

大多数传统编码不支持全部Unicode字符范围。对于这些情况,许多高级协议支持一种转义Unicode字符的方式(例如,Python本身支持 \x\u\U 约定,XML通过 &#xxx; 等支持字符引用)。

当实现这种编码算法时,Unicode对象encode方法的当前实现中存在一个问题:为了确定哪些字符不能被特定编码编码,必须尝试每个字符,因为encode不提供关于错误位置的任何信息,所以

# (1)
us = u"xxx"
s = us.encode(encoding)

必须替换为

# (2)
us = u"xxx"
v = []
for c in us:
    try:
        v.append(c.encode(encoding))
    except UnicodeError:
        v.append("&#%d;" % ord(c))
s = "".join(v)

这会大大降低编码速度,因为现在字符串的循环是在Python代码中完成的,而不是在C代码中完成的。

此外,此解决方案对有状态编码造成问题。例如,UTF-16在编码字节字符串的开头使用字节顺序标记来指定字节顺序。如果对UTF-16使用(2),则会导致在每个字符之间都有BOM的8位字符串。

为了解决这个问题,必须使用流写入器——它在调用编码函数之间保持状态

# (3)
us = u"xxx"
import codecs, cStringIO as StringIO
writer = codecs.getwriter(encoding)

v = StringIO.StringIO()
uv = writer(v)
for c in us:
    try:
        uv.write(c)
    except UnicodeError:
        uv.write(u"&#%d;" % ord(c))
s = v.getvalue()

为了比较(1)和(3)的速度,使用了以下测试脚本

# (4)
import time
us = u"äa"*1000000
encoding = "ascii"
import codecs, cStringIO as StringIO

t1 = time.time()

s1 = us.encode(encoding, "replace")

t2 = time.time()

writer = codecs.getwriter(encoding)

v = StringIO.StringIO()
uv = writer(v)
for c in us:
    try:
        uv.write(c)
    except UnicodeError:
        uv.write(u"?")
s2 = v.getvalue()

t3 = time.time()

assert(s1==s2)
print "1:", t2-t1
print "2:", t3-t2
print "factor:", (t3-t2)/(t2-t1)

在Linux上,这会给出以下输出(使用Python 2.3a0)

1: 0.274321913719
2: 51.1284689903
factor: 186.381278466

即,(3)比(1)慢180倍。

回调必须是无状态的,因为一旦回调被注册,它就在全局范围内可用,并且可以被多次 encode() 调用。为了能够使用有状态回调,encode/decode/translate的errors参数必须从 char * 更改为 PyObject *,这样就可以直接使用回调,而无需全局注册回调。由于这需要更改大量的C原型,因此此方法被拒绝。

目前所有编码/解码函数都有参数

const Py_UNICODE *p, int size

const char *p, int size

以指定要编码/解码的unicode字符/8位字符。因此,在发生错误时,编解码器必须从这些参数创建新的unicode或str对象并将其存储在异常对象中。这些编码/解码函数的调用者大部分时间都是从str/unicode对象本身提取这些参数的,因此如果直接传递这些对象,可以加快错误处理。由于这又需要更改许多C函数,因此此方法已被拒绝。

对于流读取器/写入器,errors属性必须可更改,以便在流读取器/写入器的生命周期内切换不同的错误处理方法。这目前适用于 codecs.StreamReadercodecs.StreamWriter 及其所有子类。所有核心编解码器和可能大多数第三方编解码器(例如 JapaneseCodecs)都从这些类派生它们的流读取器/写入器,因此这已经可行,但errors属性应记录为一项要求。

实现说明

一个示例实现作为SourceForge补丁#432401 [2] 提供,其中包括一个用于测试各种字符串/编码/错误组合速度的脚本和一个测试脚本。

目前,新的异常类是旧式Python类。这意味着访问属性会导致字典查找。C API的实现方式使其可以在幕后切换到新式类,如果 Exception(和 UnicodeError)将更改为用C实现的新式类以提高性能。

codecs.StreamReaderWriter 将 errors 参数用于读取和写入。为了更灵活,这可能应该更改为两个独立的读取和写入参数。

PyUnicode_TranslateCharmap 的 errors 参数对 Python 不可用,这使得无法使用 Python 脚本测试 PyUnicode_TranslateCharmap 的新功能。补丁应为 unicode.translate 添加一个可选参数 errors,以公开该功能并使测试成为可能。

如果编解码器执行与从/到unicode编码/解码不同的操作,并且想要使用新机制,则可以定义自己的异常类,并且严格处理程序将自动与其一起工作。其他预定义的错误处理程序是unicode特定的,并且期望获取 Unicode(Encode|Decode|Translate)Error 异常对象,因此它们将不起作用。

向后兼容性

unicode.encode 且 errors="replace" 的语义已更改:旧版本总是在输出字符串中存储一个 ? 字符,即使在映射中没有字符映射到 ?。使用提议的补丁,回调返回的替换字符串将再次在映射字典中查找。但由于所有支持的编码都是基于ASCII的,因此将 ? 映射到 ?,这在实践中不应成为问题。

errors 参数的非法值以前会引发 ValueError,现在它们将引发 LookupError

参考资料


来源: https://github.com/python/peps/blob/main/peps/pep-0293.rst

上次修改: 2025-02-01 08:59:27 GMT