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
이 타입은 속성마다 적절한 setter 및 getter 메서드를 갖추어 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__)
이 다섯 가지 콜백 핸들러는 Python에서도 codecs.strict_error, codecs.ignore_error, codecs.replace_error, codecs.backslashreplace_error, codecs.xmlcharrefreplace_error로 사용할 수 있습니다.
근거
대부분의 레거시 인코딩은 유니코드 문자 전체를 지원하지 않습니다. 이러한 경우를 위해 많은 고수준 프로토콜이 유니코드 문자를 이스케이프하는 방법을 지원합니다(예를 들어 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비트 문자열이 만들어집니다.
이 문제를 해결하려면 인코딩 함수 호출 사이에 상태를 유지하는 스트림 작성기(stream writer)를 사용해야 합니다:
# (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비트 문자를 지정합니다. 따라서 오류가 발생하면 코덱은 이 매개변수들로부터 새 유니코드 또는 str 객체를 만들어 예외 객체에 저장해야 합니다. 이러한 인코딩/디코딩 함수의 호출자는 대부분 str/유니코드 객체에서 직접 이 매개변수들을 추출하므로, 이 객체들을 직접 전달하면 오류 처리 속도를 높일 수 있습니다. 그러나 이 역시 많은 C 함수를 변경해야 하므로 이 접근 방식은 기각되었습니다.
스트림 리더/라이터의 경우, 스트림 리더/라이터가 살아 있는 동안 서로 다른 오류 처리 방식 사이를 전환할 수 있도록 errors 속성을 변경할 수 있어야 합니다. 현재 codecs.StreamReader와 codecs.StreamWriter 및 이들의 모든 서브클래스는 이미 그렇게 되어 있습니다. 모든 핵심 코덱과 아마도 대부분의 서드파티 코덱(예: JapaneseCodecs)이 이 클래스들로부터 스트림 리더/라이터를 파생하므로 이는 이미 동작하지만, errors 속성은 요구 사항으로 문서화되어야 합니다.
구현 참고 사항
샘플 구현은 SourceForge 패치 #432401 [2]로 제공되며, 다양한 문자열/인코딩/오류 조합의 속도를 측정하는 스크립트와 테스트 스크립트가 포함되어 있습니다.
현재 새 예외 클래스들은 구식(old style) Python 클래스입니다. 이는 속성에 접근할 때 dict 조회가 발생한다는 것을 의미합니다. 성능 향상을 위해 Exception(및 UnicodeError)이 C로 구현된 신식(new style) 클래스로 변경될 경우, 내부적으로 신식 클래스로 전환할 수 있도록 C API가 구현되어 있습니다.
codecs.StreamReaderWriter 클래스는 읽기와 쓰기 모두에 errors 매개변수를 사용합니다. 더 유연하게 하려면 이를 읽기용과 쓰기용의 두 개의 별도 매개변수로 변경하는 것이 좋을 것입니다.
PyUnicode_TranslateCharmap의 errors 매개변수는 Python에서 사용할 수 없으므로, Python 스크립트로는 PyUnicode_TranslateCharmap의 새 기능을 테스트할 수 없습니다. 이 패치는 해당 기능을 노출하고 테스트를 가능하게 하기 위해 unicode.translate에 선택적 인자 errors를 추가해야 합니다.
유니코드로부터/유니코드로의 인코딩/디코딩과는 다른 작업을 하면서 새 메커니즘을 사용하려는 코덱은 자체 예외 클래스를 정의할 수 있으며, strict 핸들러는 자동으로 그 예외 클래스와 함께 동작합니다. 미리 정의된 다른 오류 핸들러들은 유니코드 전용이며 Unicode(Encode|Decode|Translate)Error 예외 객체를 받을 것으로 기대하므로 동작하지 않습니다.
하위 호환성
errors=”replace”를 지정한 unicode.encode의 의미가 변경되었습니다: 이전 버전은 매핑에서 어떤 문자도 ?에 매핑되지 않은 경우에도 항상 출력 문자열에 ? 문자를 저장했습니다. 제안된 패치에서는 콜백이 반환한 대체 문자열을 다시 매핑 딕셔너리에서 조회합니다. 그러나 지원되는 모든 인코딩은 ASCII 기반이므로 ?를 ?로 매핑하며, 따라서 실제로는 문제가 되지 않을 것입니다.
이전에는 errors 인자에 잘못된 값을 지정하면 ValueError가 발생했지만, 이제는 LookupError가 발생합니다.
참고 문헌
Copyright
This document has been placed in the public domain.