PEP 293 – Codec Error Handling Callbacks
- Autor:
- Walter Dörwald <walter at livinglogic.de>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 18-Jun-2002
- Python-Version:
- 2.3
- Post-History:
- 19-Jun-2002
Zusammenfassung
Dieses PEP zielt darauf ab, Pythons fest verdrahtete Codec-Fehlerbehandlungsmechanismen um einen flexibleren Callback-basierten Ansatz zu erweitern.
Python verwendet derzeit eine feste Fehlerbehandlung für Codec-Fehlerhandler. Dieses PEP beschreibt einen Mechanismus, der es Python ermöglicht, Funktions-Callbacks als Fehlerhandler zu verwenden. Mit diesen flexibleren Fehlerhandlern ist es möglich, bestehenden Codecs neue Funktionalität hinzuzufügen, z. B. durch die Bereitstellung von Fallback-Lösungen oder unterschiedlichen Kodierungen für Fälle, in denen die Standard-Codec-Zuordnung nicht zutrifft.
Spezifikation
Derzeit ist die Menge der Codec-Fehlerbehandlungsalgorithmen auf "strict", "replace" oder "ignore" beschränkt, und die Semantik dieser Algorithmen wird für jeden Codec separat implementiert.
Der vorgeschlagene Patch macht die Menge der Fehlerbehandlungsalgorithmen durch eine Registry für Codec-Fehlerhandler erweiterbar, die Handler-Namen auf Handler-Funktionen abbildet. Diese Registry besteht aus den folgenden zwei C-Funktionen
int PyCodec_RegisterError(const char *name, PyObject *error)
PyObject *PyCodec_LookupError(const char *name)
und ihren Python-Entsprechungen
codecs.register_error(name, error)
codecs.lookup_error(name)
PyCodec_LookupError löst eine LookupError aus, wenn unter diesem Namen keine Callback-Funktion registriert wurde.
Ähnlich wie bei der Registry für Kodierungsnamen gibt es keine Möglichkeit, Callback-Funktionen zu deregistrieren oder die verfügbaren Funktionen zu durchlaufen.
Die Callback-Funktionen werden von den Codecs auf folgende Weise verwendet: Wenn der Codec einen Kodierungs-/Dekodierungsfehler feststellt, wird die Callback-Funktion nach Namen nachgeschlagen, die Informationen über den Fehler werden in einem Ausnahmeobjekt gespeichert und der Callback wird mit diesem Objekt aufgerufen. Der Callback gibt Informationen darüber zurück, wie fortzufahren ist (oder löst eine Ausnahme aus).
Für die Kodierung sieht das Ausnahmeobjekt wie folgt aus
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
Dieser Typ wird in C mit den entsprechenden Setter- und Getter-Methoden für die Attribute implementiert, die folgende Bedeutung haben
encoding: Der Name der Kodierung;object: Das ursprüngliche Unicode-Objekt, für dasencode()aufgerufen wurde;start: Die Position des ersten nicht kodierbaren Zeichens;end: (Die Position des letzten nicht kodierbaren Zeichens)+1 (oder die Länge des Objekts, wenn alle Zeichen von start bis zum Ende des Objekts nicht kodierbar sind);reason: Der Grund, warumobject[start:end]nicht kodiert werden konnte.
Wenn das Objekt aufeinanderfolgende nicht kodierbare Zeichen enthält, sollte der Encoder diese Zeichen für einen einzigen Aufruf des Callbacks sammeln, wenn diese Zeichen aus demselben Grund nicht kodiert werden können. Der Encoder ist nicht verpflichtet, dieses Verhalten zu implementieren, kann aber den Callback für jedes einzelne Zeichen aufrufen, es wird jedoch dringend empfohlen, die Sammelmethode zu implementieren.
Der Callback darf das Ausnahmeobjekt nicht ändern. Wenn der Callback keine Ausnahme auslöst (entweder die übergebene oder eine andere), muss er ein Tupel zurückgeben
(replacement, newpos)
replacement ist ein Unicode-Objekt, das der Encoder kodieren und ausgeben wird anstelle des nicht kodierbaren Teils object[start:end], newpos gibt eine neue Position innerhalb von object an, an der (nachdem der Ersatz kodiert wurde) der Encoder die Kodierung fortsetzen wird.
Negative Werte für newpos werden als relativ zum Ende des Objekts behandelt. Wenn newpos außerhalb des gültigen Bereichs liegt, löst der Encoder eine IndexError aus.
Wenn die Ersatzzeichenfolge selbst ein nicht kodierbares Zeichen enthält, löst der Encoder das Ausnahmeobjekt aus (kann aber vor dem Auslösen eine andere Reason-Zeichenfolge festlegen).
Sollten weitere Kodierungsfehler auftreten, darf der Encoder das Ausnahmeobjekt für den nächsten Aufruf des Callbacks wiederverwenden. Darüber hinaus darf der Encoder das Ergebnis von codecs.lookup_error zwischenspeichern.
Wenn der Callback nicht weiß, wie die Ausnahme behandelt werden soll, muss er eine TypeError auslösen.
Das Dekodieren funktioniert ähnlich wie das Kodieren mit den folgenden Unterschieden
- Die Ausnahmeklasse heißt
UnicodeDecodeErrorund das Attribut object ist die ursprüngliche 8-Bit-Zeichenkette, die der Decoder gerade dekodiert. - Der Decoder ruft den Callback mit den Bytes auf, die eine nicht dekodierbare Sequenz darstellen, auch wenn es mehr als eine nicht dekodierbare Sequenz gibt, die aus demselben Grund direkt nach der ersten nicht dekodierbar ist. Z. B. bei der Kodierung "unicode-escape", beim Dekodieren des illegalen Strings
\\u00\\u01x, wird der Callback zweimal aufgerufen (einmal für\\u00und einmal für\\u01). Dies geschieht, um die korrekte Anzahl von Ersatzzeichen generieren zu können. - Der vom Callback zurückgegebene Ersatz ist ein Unicode-Objekt, das vom Decoder als-is ohne weitere Verarbeitung anstelle des nicht dekodierbaren Teils
object[start:end]ausgegeben wird.
Es gibt eine dritte API, die das alte strikte/ignoriere/ersetze Fehlerbehandlungsschema verwendet
PyUnicode_TranslateCharmap/unicode.translate
Der vorgeschlagene Patch erweitert PyUnicode_TranslateCharmap so, dass es auch die Callback-Registry unterstützt. Dies hat den zusätzlichen Nebeneffekt, dass PyUnicode_TranslateCharmap Multi-Zeichen-Ersetzungsstrings unterstützt (siehe SF Feature Request #403100 [1]).
Für PyUnicode_TranslateCharmap heißt die Ausnahmeklasse UnicodeTranslateError. PyUnicode_TranslateCharmap sammelt alle aufeinanderfolgenden nicht übersetzbaren Zeichen (d. h. solche, die auf None abgebildet werden) und ruft den Callback mit ihnen auf. Der vom Callback zurückgegebene Ersatz ist ein Unicode-Objekt, das als-is in das übersetzte Ergebnis eingefügt wird, ohne weitere Verarbeitung.
Alle Encoder und Decoder dürfen die Callback-Funktionalität selbst implementieren, wenn sie den Callback-Namen erkennen (d. h. wenn es sich um einen System-Callback wie "strict", "replace" und "ignore" handelt). Der vorgeschlagene Patch fügt zwei zusätzliche System-Callback-Namen hinzu: "backslashreplace" und "xmlcharrefreplace", die für die Kodierung und Übersetzung verwendet werden können und die ebenfalls für alle Encoder und PyUnicode_TranslateCharmap inplace implementiert werden.
Die Python-Entsprechungen dieser fünf Callbacks sehen wie folgt aus
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__)
Diese fünf Callback-Handler sind auch für Python unter codecs.strict_error, codecs.ignore_error, codecs.replace_error, codecs.backslashreplace_error und codecs.xmlcharrefreplace_error zugänglich.
Begründung
Die meisten Legacy-Kodierungen unterstützen nicht den vollen Bereich von Unicode-Zeichen. Für diese Fälle unterstützen viele High-Level-Protokolle eine Möglichkeit, ein Unicode-Zeichen zu escapen (z. B. unterstützt Python selbst die Konvention \x, \u und \U, XML unterstützt Zeichenreferenzen über &#xxx; usw.).
Bei der Implementierung eines solchen Kodierungsalgorithmus zeigt sich ein Problem mit der aktuellen Implementierung der encode-Methode von Unicode-Objekten: Um festzustellen, welche Zeichen von einer bestimmten Kodierung nicht kodiert werden können, muss jedes einzelne Zeichen ausprobiert werden, da encode keine Informationen über die Position des/der Fehler liefert, sodass
# (1)
us = u"xxx"
s = us.encode(encoding)
ersetzt werden muss durch
# (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)
Dies verlangsamt die Kodierung dramatisch, da die Schleife durch die Zeichenkette nun im Python-Code und nicht mehr im C-Code erfolgt.
Darüber hinaus birgt diese Lösung Probleme mit zustandsbehafteten Kodierungen. UTF-16 verwendet beispielsweise eine Byte Order Mark (BOM) am Anfang der kodierten Byte-Zeichenkette, um die Byte-Reihenfolge anzugeben. Die Verwendung von (2) mit UTF-16 führt zu einer 8-Bit-Zeichenkette mit einer BOM zwischen jedem Zeichen.
Um dieses Problem zu umgehen, muss ein Stream-Writer – der den Zustand zwischen den Aufrufen der Kodierungsfunktion beibehält – verwendet werden
# (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()
Zum Vergleichen der Geschwindigkeit von (1) und (3) wurde das folgende Testskript verwendet
# (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)
Unter Linux ergibt dies die folgende Ausgabe (mit Python 2.3a0)
1: 0.274321913719
2: 51.1284689903
factor: 186.381278466
d. h. (3) ist 180-mal langsamer als (1).
Callbacks müssen zustandslos sein, da sie, sobald sie registriert sind, global verfügbar sind und von mehreren encode()-Aufrufen aufgerufen werden können. Um zustandsbehaftete Callbacks verwenden zu können, müsste der errors-Parameter für encode/decode/translate von char * auf PyObject * geändert werden, damit der Callback direkt, ohne die Notwendigkeit, den Callback global zu registrieren, verwendet werden könnte. Da dies Änderungen an vielen C-Prototypen erfordert, wurde dieser Ansatz abgelehnt.
Derzeit haben alle Kodierungs-/Dekodierungsfunktionen Argumente
const Py_UNICODE *p, int size
oder
const char *p, int size
um die zu kodierenden/dekodierenden Unicode-Zeichen/8-Bit-Zeichen anzugeben. Im Fehlerfall muss der Codec also ein neues Unicode- oder str-Objekt aus diesen Parametern erstellen und im Ausnahmeobjekt speichern. Die Aufrufer dieser Kodierungs-/Dekodierungsfunktionen extrahieren diese Parameter meist selbst aus str/unicode-Objekten, daher könnte die Fehlerbehandlung beschleunigt werden, wenn diese Objekte direkt übergeben würden. Da dies wiederum Änderungen an vielen C-Funktionen erfordert, wurde dieser Ansatz abgelehnt.
Für Stream-Reader/-Writer muss das errors-Attribut änderbar sein, um während der Lebensdauer des Stream-Readers/-Writers zwischen verschiedenen Fehlerbehandlungsmethoden wechseln zu können. Dies ist derzeit bei codecs.StreamReader und codecs.StreamWriter und all ihren Unterklassen der Fall. Alle Kern-Codecs und wahrscheinlich die meisten Drittanbieter-Codecs (z. B. JapaneseCodecs) leiten ihre Stream-Reader/-Writer von diesen Klassen ab, daher funktioniert dies bereits, aber das Attribut errors sollte als Anforderung dokumentiert werden.
Implementierungs-Hinweise
Eine Beispielimplementierung ist als SourceForge-Patch #432401 [2] verfügbar, einschließlich eines Skripts zum Testen der Geschwindigkeit verschiedener Zeichenketten-/Kodierungs-/Fehlerkombinationen und eines Testskripts.
Derzeit sind die neuen Ausnahme-Klassen alte Python-Klassen. Das bedeutet, dass der Zugriff auf Attribute einen Dictionary-Lookup bewirkt. Die C-API ist so implementiert, dass es möglich ist, hinter den Kulissen zu neuen Klassen zu wechseln, wenn Exception (und UnicodeError) zu neuen Klassen werden, die in C implementiert sind, um die Leistung zu verbessern.
Die Klasse codecs.StreamReaderWriter verwendet den errors-Parameter sowohl für das Lesen als auch für das Schreiben. Um flexibler zu sein, sollte dies wahrscheinlich auf zwei separate Parameter für das Lesen und Schreiben geändert werden.
Der errors-Parameter von PyUnicode_TranslateCharmap ist für Python nicht zugänglich, was das Testen der neuen Funktionalität von PyUnicode_TranslateCharmap mit Python-Skripten unmöglich macht. Der Patch sollte ein optionales Argument errors zu unicode.translate hinzufügen, um die Funktionalität verfügbar zu machen und das Testen zu ermöglichen.
Codecs, die etwas anderes tun als das Kodieren/Dekodieren von/nach Unicode und die neue Mechanik nutzen möchten, können ihre eigenen Ausnahme-Klassen definieren, und die strikten Handler funktionieren automatisch damit. Die anderen vordefinierten Fehlerhandler sind Unicode-spezifisch und erwarten, ein Unicode(Encode|Decode|Translate)Error-Ausnahmeobjekt zu erhalten, daher funktionieren sie nicht.
Abwärtskompatibilität
Die Semantik von unicode.encode mit errors="replace" hat sich geändert: Die alte Version speicherte immer ein ?-Zeichen in der Ausgabestring, auch wenn kein Zeichen zu ? im Mapping abgebildet wurde. Mit dem vorgeschlagenen Patch wird der Ersetzungsstring vom Callback erneut im Mapping-Dictionary nachgeschlagen. Da aber alle unterstützten Kodierungen ASCII-basiert sind und somit ? auf ? abbilden, sollte dies in der Praxis kein Problem darstellen.
Illegale Werte für das errors-Argument lösten zuvor ValueError aus, nun lösen sie LookupError aus.
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0293.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT