PEP 293 – 编解码错误处理回调
- 作者:
- Walter Dörwald <walter at livinglogic.de>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2002-06-18
- Python 版本:
- 2.3
- 历史记录:
- 2002-06-19
摘要
本 PEP 的目标是使用更灵活的基于回调的方法扩展 Python 的固定编解码错误处理方案。
Python 当前对编解码错误处理程序使用固定错误处理。本 PEP 描述了一种机制,它允许 Python 使用函数回调作为错误处理程序。使用这些更灵活的错误处理程序,可以通过例如提供回退解决方案或不同编码来为标准编解码映射不适用的情况添加新的功能。
规范
目前,编解码错误处理算法集固定为“严格”、“替换”或“忽略”,这些算法的语义在每个编解码器中单独实现。
提出的补丁将通过编解码错误处理程序注册表使错误处理算法集可扩展,该注册表将处理程序名称映射到处理程序函数。该注册表包含以下两个 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(或对象长度,如果从开始到对象结束的所有字符都不可编码);reason
:object[start:end]
无法编码的原因。
如果对象包含连续的不可编码字符,则编码器应收集这些字符以进行一次回调调用,如果这些字符无法出于相同原因进行编码。编码器不需要实现此行为,但可以对每个字符调用回调,但强烈建议实现收集方法。
回调不得修改异常对象。如果回调没有引发异常(传入的异常或其他异常),它必须返回一个元组
(replacement, newpos)
replacement 是编码器将编码并发出以代替不可编码 object[start:end]
部分的 Unicode 对象,newpos 指定对象中一个新的位置,其中(在编码替换后)编码器将继续编码。
newpos 的负值将被视为相对于对象结束。如果 newpos 超出界限,编码器将引发 IndexError
。
如果替换字符串本身包含不可编码字符,编码器会引发异常对象(但在引发之前可以设置不同的原因字符串)。
如果发生其他编码错误,编码器允许在下次调用回调时重用异常对象。此外,编码器允许缓存 codecs.lookup_error
的结果。
如果回调不知道如何处理异常,它必须引发 TypeError
。
解码与编码类似,但有以下区别
- 异常类名为
UnicodeDecodeError
,属性 object 是解码器当前正在解码的原始 8 位字符串。 - 解码器将使用构成一个不可解码序列的字节调用回调,即使直接在第一个不可解码序列之后存在多个不可解码序列也无法出于相同原因进行解码。例如,对于“unicode-escape”编码,在解码非法字符串
\\u00\\u01x
时,将调用回调两次(一次针对\\u00
,一次针对\\u01
)。这样做是为了能够生成正确数量的替换字符。 - 从回调返回的替换是一个 Unicode 对象,解码器将按原样发出,不会进行进一步处理,而不是不可解码的
object[start:end]
部分。
还有一个使用旧的严格/忽略/替换错误处理方案的第三个 API
PyUnicode_TranslateCharmap/unicode.translate
提出的补丁将增强 PyUnicode_TranslateCharmap
,以便它也支持回调注册表。这具有额外的副作用,即 PyUnicode_TranslateCharmap
将支持多字符替换字符串(参见 SF 功能请求 #403100 [1])。
对于 PyUnicode_TranslateCharmap
,异常类将名为 UnicodeTranslateError
。 PyUnicode_TranslateCharmap
将收集所有连续的不可翻译字符(即那些映射到 None
的字符)并使用它们调用回调。从回调返回的替换是一个 Unicode 对象,它将按原样放入翻译结果中,不会进行进一步处理。
所有编码器和解码器都允许自己实现回调功能,如果它们识别回调名称(即,如果它是系统回调,如“严格”、“替换”和“忽略”)。提出的补丁将添加两个额外的系统回调名称:“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_error
、codecs.ignore_error
、codecs.replace_error
、codecs.backslashreplace_error
和 codecs.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 在编码字节字符串的开头使用字节顺序标记来指定字节顺序。使用 (2) 与 UTF-16,会导致在每个字符之间有一个 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.StreamReader
和 codecs.StreamWriter
及其所有子类。所有核心编解码器,以及可能的大多数第三方编解码器(例如 JapaneseCodecs
)都从这些类派生它们的流读取器/写入器,因此这已经可以工作,但应将 errors 属性记录为一项要求。
实施说明
示例实现可作为 SourceForge 补丁 #432401 [2] 获得,其中包括一个用于测试各种字符串/编码/错误组合速度的脚本和一个测试脚本。
当前新的异常类是旧式 Python 类。这意味着访问属性会导致字典查找。C API 的实现方式使得在幕后切换到新式类成为可能,如果 Exception
(和 UnicodeError
)将被更改为在 C 中实现的新式类,以提高性能。
类 codecs.StreamReaderWriter
对读写都使用 errors 参数。为了更灵活,应该将其更改为两个独立的读写参数。
PyUnicode_TranslateCharmap
的 errors 参数在 Python 中不可用,这使得使用 Python 脚本测试 PyUnicode_TranslateCharmap
的新功能变得不可能。该补丁应该添加一个可选参数 errors 到 unicode.translate 以公开该功能并使测试成为可能。
执行与从 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
上次修改时间:2023-09-09 17:39:29 GMT