PEP 293 – 코덱 오류 처리 콜백
- Author:
- Walter Dörwald <walter at livinglogic.de>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 18-Jun-2002
- Python-Version:
- 2.3
- Post-History:
- 19-Jun-2002
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 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()가 호출된 원래 유니코드 객체입니다.start: 인코딩할 수 없는 첫 번째 문자의 위치입니다.end: 인코딩할 수 없는 마지막 문자의 위치에 1을 더한 값입니다(또는 start부터 object의 끝까지 모든 문자를 인코딩할 수 없는 경우 object의 길이입니다).reason:object[start:end]를 인코딩할 수 없는 이유입니다.
object에 연속된 인코딩할 수 없는 문자가 있는 경우, 해당 문자들을 같은 이유로 인코딩할 수 없다면 인코더는 한 번의 콜백 호출을 위해 해당 문자들을 모아야 합니다. 인코더가 이 동작을 구현해야 하는 것은 아니며 각 문자마다 콜백을 호출할 수도 있지만, 수집 방식을 구현하는 것이 강력히 권장됩니다.
콜백은 예외 객체를 수정해서는 안 됩니다. 콜백이 예외를 발생시키지 않는 경우(전달된 예외이든 다른 예외이든), 튜플을 반환해야 합니다.:
(replacement, newpos)
replacement는 인코더가 인코딩할 수 없는 object[start:end] 부분 대신 인코딩하여 출력할 유니코드 객체이며, newpos는 object 내의 새로운 위치를 지정합니다. 인코더는 replacement를 인코딩한 후 이 위치부터 인코딩을 계속합니다.
newpos의 음수 값은 object의 끝을 기준으로 한 값으로 처리됩니다. newpos가 범위를 벗어나면 인코더는 IndexError를 발생시킵니다.
replacement 문자열 자체에 인코딩할 수 없는 문자가 포함되어 있으면 인코더는 예외 객체를 발생시키지만, 발생시키기 전에 다른 reason 문자열을 설정할 수 있습니다.
추가 인코딩 오류가 발생하는 경우 인코더는 다음 콜백 호출에서 해당 예외 객체를 재사용할 수 있습니다. 또한 인코더는 codecs.lookup_error의 결과를 캐시할 수 있습니다.
콜백이 예외를 처리하는 방법을 모르는 경우 TypeError를 발생시켜야 합니다.
디코딩은 다음과 같은 차이점을 제외하면 인코딩과 유사하게 작동합니다.
- 예외 클래스의 이름은
UnicodeDecodeError이며, object 속성은 디코더가 현재 디코딩하고 있는 원본 8비트 문자열입니다. - 디코더는 디코딩할 수 없는 하나의 시퀀스를 구성하는 바이트를 콜백에 전달하여 호출합니다. 첫 번째 시퀀스 바로 뒤에 같은 이유로 디코딩할 수 없는 시퀀스가 둘 이상 있더라도 마찬가지입니다. 예를 들어 “unicode-escape” 인코딩에서 잘못된 문자열
\\u00\\u01x를 디코딩할 때 콜백은 두 번 호출됩니다(\\u00에 대해 한 번,\\u01에 대해 한 번). 이는 올바른 수의 대체 문자를 생성할 수 있도록 하기 위한 것입니다. - 콜백이 반환하는 대체 값은 디코더가 디코딩할 수 없는
object[start:end]부분 대신 추가 처리 없이 있는 그대로 출력하는 유니코드 객체입니다.
이전의 strict/ignore/replace 오류 처리 방식을 사용하는 세 번째 API도 있습니다.:
PyUnicode_TranslateCharmap/unicode.translate
제안된 패치는 PyUnicode_TranslateCharmap을 확장하여 콜백 레지스트리도 지원하도록 합니다. 이로 인해 PyUnicode_TranslateCharmap이 다중 문자 대체 문자열도 지원하게 되는 추가 효과가 있습니다(SF 기능 요청 #403100 [1] 참조).
PyUnicode_TranslateCharmap에서 예외 클래스의 이름은 UnicodeTranslateError입니다. PyUnicode_TranslateCharmap은 연속된 모든 변환 불가능 문자를 수집합니다(즉, None에 매핑되는 문자). 그런 다음 해당 문자들을 콜백에 전달하여 호출합니다. 콜백이 반환하는 대체 값은 추가 처리 없이 있는 그대로 변환 결과에 삽입되는 유니코드 객체입니다.
모든 인코더와 디코더는 콜백 이름을 인식하는 경우(즉, “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_error, codecs.ignore_error, codecs.replace_error, codecs.backslashreplace_error 및 codecs.xmlcharrefreplace_error로 Python에서 접근할 수도 있습니다.
근거입니다.
대부분의 레거시 인코딩은 유니코드 문자의 전체 범위를 지원하지 않습니다. 이러한 경우 많은 고수준 프로토콜은 유니코드 문자를 이스케이프하는 방법을 지원합니다(예를 들어 Python 자체는 \x, \u 및 \U 규칙을 지원하고, XML은 &#xxx; 등을 통한 문자 참조를 지원합니다).
이러한 인코딩 알고리즘을 구현할 때 유니코드 객체의 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)
이제 문자열을 순회하는 루프가 더 이상 C 코드가 아니라 Python 코드에서 수행되므로 인코딩 속도가 크게 느려집니다.
더욱이 이 해결책은 상태 유지 인코딩에 문제를 일으킵니다. 예를 들어 UTF-16은 인코딩된 바이트 문자열의 시작 부분에 Byte Order Mark를 사용하여 바이트 순서를 지정합니다. 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
인코딩/디코딩될 유니코드 문자/8비트 문자를 지정합니다. 따라서 오류가 발생하면 코덱은 이 매개변수들로부터 새로운 unicode 또는 str 객체를 만들어 예외 객체에 저장해야 합니다. 이러한 인코딩/디코딩 함수의 호출자는 대부분의 경우 이 매개변수들을 str/unicode 객체 자체에서 추출하므로, 이 객체를 직접 전달하면 오류 처리 속도를 높일 수 있습니다. 이 또한 많은 C 함수의 변경을 요구하므로, 이 접근 방식은 거부되었습니다.
스트림 리더/라이터의 경우, 스트림 리더/라이터의 수명 동안 서로 다른 오류 처리 방식 간을 전환할 수 있도록 errors 속성을 변경 가능해야 합니다. 이는 현재 codecs.StreamReader와 codecs.StreamWriter, 그리고 이 클래스들의 모든 서브클래스에 해당합니다. 모든 핵심 코덱과 아마도 대부분의 서드파티 코덱(예: JapaneseCodecs)은 이 클래스들로부터 스트림 리더/라이터를 파생시키므로 이는 이미 작동하지만, errors 속성은 요구 사항으로 문서화되어야 합니다.
구현 노트
샘플 구현은 SourceForge 패치 #432401 [2]로 제공되며, 다양한 문자열/인코딩/오류 조합의 속도를 테스트하는 스크립트와 테스트 스크립트를 포함합니다.
현재 새로운 예외 클래스는 구식(old-style) Python 클래스입니다. 이는 속성에 접근하면 딕셔너리 조회가 발생함을 의미합니다. C API는 성능 향상을 위해 Exception (및 UnicodeError)이 C로 구현된 new-style 클래스로 변경될 경우, 내부적으로 new-style 클래스로 전환할 수 있도록 구현되어 있습니다.
codecs.StreamReaderWriter 클래스는 읽기와 쓰기 모두에 errors 매개변수를 사용합니다. 더 유연하게 만들기 위해, 이는 아마도 읽기용과 쓰기용의 두 개의 별도 매개변수로 변경되어야 할 것입니다.
PyUnicode_TranslateCharmap의 errors 매개변수는 Python에서 사용할 수 없으며, 이로 인해 PyUnicode_TranslateCharmap의 새 기능을 Python 스크립트로 테스트하는 것이 불가능합니다. 이 패치는 해당 기능을 노출하고 테스트를 가능하게 하기 위해 unicode.translate에 선택적 인자 errors를 추가해야 합니다.
unicode로부터/로의 인코딩·디코딩과는 다른 작업을 수행하면서 새로운 메커니즘을 사용하고자 하는 코덱은 자체 예외 클래스를 정의할 수 있으며, strict 처리기는 자동으로 이와 함께 동작합니다. 다른 미리 정의된 오류 처리기들은 unicode 전용이며 Unicode(Encode|Decode|Translate)Error 예외 객체를 받을 것으로 예상하므로 동작하지 않습니다.
하위 호환성
errors=”replace”를 사용하는 unicode.encode의 의미가 변경되었습니다: 이전 버전은 매핑에서 어떤 문자도 ?로 매핑되지 않은 경우에도 항상 출력 문자열에 ? 문자를 저장했습니다. 제안된 패치를 적용하면 콜백에서 반환된 대체 문자열은 다시 매핑 딕셔너리에서 조회됩니다. 하지만 지원되는 모든 인코딩은 ASCII 기반이며 따라서 ?를 ?로 매핑하므로, 실제로는 문제가 되지 않아야 합니다.
이전에는 errors 인자에 잘못된 값을 지정하면 ValueError가 발생했지만, 이제는 LookupError가 발생합니다.
참고 자료
Copyright
This document has been placed in the public domain.