PEP 393 – Flexible String Representation
- Autor:
- Martin von Löwis <martin at v.loewis.de>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 24. Jan. 2010
- Python-Version:
- 3.3
- Post-History:
Zusammenfassung
Der interne Darstellungstyp für Unicode-Strings wird geändert, um mehrere interne Darstellungen zu unterstützen, abhängig von dem Zeichen mit der größten Unicode-Ordnungszahl (1, 2 oder 4 Bytes). Dies ermöglicht eine speichereffiziente Darstellung in gängigen Fällen, erlaubt aber den Zugriff auf volles UCS-4 auf allen Systemen. Zur Kompatibilität mit bestehenden APIs können mehrere Darstellungen parallel existieren; mit der Zeit sollte diese Kompatibilität auslaufen. Die Unterscheidung zwischen schmalen und breiten Unicode-Builds wird aufgehoben. Eine Implementierung dieses PEP ist verfügbar unter [1].
Begründung
Es gibt zwei Arten von Beschwerden über die aktuelle Implementierung des Unicode-Typs: Auf Systemen, die nur UTF-16 unterstützen, beschweren sich Benutzer, dass Nicht-BMP-Zeichen nicht richtig unterstützt werden. Auf Systemen, die intern UCS-4 verwenden (und manchmal auch auf Systemen, die UCS-2 verwenden), gibt es die Beschwerde, dass Unicode-Strings zu viel Speicher verbrauchen – insbesondere im Vergleich zu Python 2.x, wo derselbe Code oft ASCII-Strings (d.h. ASCII-kodierte Byte-Strings) verwendet hätte. Mit dem vorgeschlagenen Ansatz werden reine ASCII-Unicode-Strings wieder nur ein Byte pro Zeichen verbrauchen; gleichzeitig wird weiterhin ein effizientes Indizieren von Strings ermöglicht, die Nicht-BMP-Zeichen enthalten (da Strings, die diese enthalten, 4 Bytes pro Zeichen verwenden werden).
Ein Problem des Ansatzes ist die Unterstützung bestehender Anwendungen (z. B. Erweiterungsmodule). Zur Kompatibilität können redundante Darstellungen berechnet werden. Anwendungen werden ermutigt, die Abhängigkeit von einer bestimmten internen Darstellung, wenn möglich, auslaufen zu lassen. Da die Interaktion mit anderen Bibliotheken oft eine Art interner Darstellung erfordert, wählt die Spezifikation UTF-8 als empfohlene Methode, um Strings für C-Code bereitzustellen.
Für viele Strings (z. B. ASCII) können mehrere Darstellungen tatsächlich Speicher teilen (z. B. kann die kürzeste Form mit der UTF-8-Form geteilt werden, wenn alle Zeichen ASCII sind). Mit solchem Teilen wird der Overhead von Kompatibilitätsdarstellungen reduziert. Wenn Darstellungen Daten teilen, ist es auch möglich, Strukturfelder wegzulassen, wodurch die Basisgröße von String-Objekten reduziert wird.
Spezifikation
Unicode-Strukturen sind nun als Hierarchie von Strukturen definiert, nämlich
typedef struct {
PyObject_HEAD
Py_ssize_t length;
Py_hash_t hash;
struct {
unsigned int interned:2;
unsigned int kind:2;
unsigned int compact:1;
unsigned int ascii:1;
unsigned int ready:1;
} state;
wchar_t *wstr;
} PyASCIIObject;
typedef struct {
PyASCIIObject _base;
Py_ssize_t utf8_length;
char *utf8;
Py_ssize_t wstr_length;
} PyCompactUnicodeObject;
typedef struct {
PyCompactUnicodeObject _base;
union {
void *any;
Py_UCS1 *latin1;
Py_UCS2 *ucs2;
Py_UCS4 *ucs4;
} data;
} PyUnicodeObject;
Objekte, für die sowohl die Größe als auch das maximale Zeichen zur Erstellungszeit bekannt sind, werden als „kompakte“ Unicode-Objekte bezeichnet; die Zeichendaten folgen unmittelbar der Basisstruktur. Wenn das maximale Zeichen kleiner als 128 ist, verwenden sie die Struktur PyASCIIObject, und die UTF-8-Daten, die UTF-8-Länge und die wstr-Länge sind gleich der Länge der ASCII-Daten. Für Nicht-ASCII-Strings wird die Struktur PyCompactObject verwendet. Das Ändern der Größe von kompakten Objekten wird nicht unterstützt.
Objekte, für die das maximale Zeichen nicht zur Erstellungszeit angegeben wird, werden als „Legacy“-Objekte bezeichnet und durch PyUnicode_FromStringAndSize(NULL, length) erstellt. Sie verwenden die Struktur PyUnicodeObject. Anfangs sind ihre Daten nur im wstr-Zeiger enthalten; wenn PyUnicode_READY aufgerufen wird, wird der Datenzeiger (Union) zugewiesen. Eine Größenänderung ist möglich, solange PyUnicode_READY nicht aufgerufen wurde.
Die Felder haben folgende Interpretationen:
- length: Anzahl der Code Points im String (Ergebnis von sq_length)
- interned: Internierungsstatus (SSTATE_*) wie in 3.2
- kind: Form des Strings
- 00 => str ist nicht initialisiert (Daten sind in wstr)
- 01 => 1 Byte (Latin-1)
- 10 => 2 Bytes (UCS-2)
- 11 => 4 Bytes (UCS-4);
- compact: Das Objekt verwendet eine der kompakten Darstellungen (impliziert ready)
- ascii: Das Objekt verwendet die PyASCIIObject-Darstellung (impliziert compact und ready)
- ready: Die kanonische Darstellung ist zur Verfügung gestellt unter PyUnicode_DATA und PyUnicode_GET_LENGTH. Dies wird entweder gesetzt, wenn das Objekt kompakt ist, oder wenn der Datenzeiger und die Länge initialisiert wurden.
- wstr_length, wstr: Darstellung in der plattformabhängigen wchar_t (null-terminiert). Wenn wchar_t 16-Bit ist, kann diese Form Ersatzpaare verwenden (in diesem Fall unterscheidet sich wstr_length von length). wstr_length unterscheidet sich von length nur, wenn Ersatzpaare in der Darstellung vorhanden sind.
- utf8_length, utf8: UTF-8-Darstellung (null-terminiert).
- data: Darstellung des Unicode-Strings in der kürzesten Form. Der String ist null-terminiert (in seiner jeweiligen Darstellung).
Alle drei Darstellungen sind optional, obwohl die Datenform als kanonische Darstellung gilt, die nur während der Erstellung des Strings abwesend sein kann. Wenn die Darstellung fehlt, ist der Zeiger NULL und das entsprechende Längenfeld kann beliebige Daten enthalten.
Der Typ Py_UNICODE wird weiterhin unterstützt, ist aber veraltet. Er wird immer als Typedef für wchar_t definiert, sodass die wstr-Darstellung als Py_UNICODE-Darstellung verwendet werden kann.
Die Zeiger data und utf8 zeigen auf denselben Speicher, wenn der String nur ASCII-Zeichen verwendet (nur Latin-1 reicht nicht aus). Die Zeiger data und wstr zeigen auf denselben Speicher, wenn der String genau in den wchar_t-Typ der Plattform passt (d.h., verwendet einige BMP-nicht-Latein-1-Zeichen, wenn sizeof(wchar_t) 2 ist, und verwendet einige Nicht-BMP-Zeichen, wenn sizeof(wchar_t) 4 ist).
String-Erstellung
Die empfohlene Methode zur Erstellung eines Unicode-Objekts ist die Verwendung der Funktion PyUnicode_New
PyObject* PyUnicode_New(Py_ssize_t size, Py_UCS4 maxchar);
Beide Parameter müssen die endgültige Größe/den Bereich der Strings angeben. Insbesondere müssen Codecs, die diese API verwenden, sowohl die Anzahl der Zeichen als auch das maximale Zeichen im Voraus berechnen. Ein String wird entsprechend der angegebenen Größe und des Zeichenbereichs zugewiesen und ist null-terminiert; die tatsächlichen Zeichen darin können uninitialisiert sein.
PyUnicode_FromString und PyUnicode_FromStringAndSize bleiben für die Verarbeitung von UTF-8-Eingaben unterstützt; die Eingabe wird dekodiert, und die UTF-8-Darstellung ist für den String noch nicht gesetzt.
PyUnicode_FromUnicode bleibt unterstützt, ist aber veraltet. Wenn der Py_UNICODE-Zeiger nicht Null ist, wird die Datenrepräsentation gesetzt. Wenn der Zeiger Null ist, wird eine passend große wstr-Darstellung zugewiesen, die geändert werden kann, bis PyUnicode_READY() aufgerufen wird (explizit oder implizit). Die Größenänderung eines Unicode-Strings bleibt möglich, bis er finalisiert ist.
PyUnicode_READY() konvertiert einen String, der nur eine wstr-Darstellung enthält, in die kanonische Darstellung. Sofern wstr und data keinen Speicher teilen können, wird die wstr-Darstellung nach der Konvertierung verworfen. Das Makro gibt 0 bei Erfolg und -1 bei Fehler zurück, was insbesondere bei fehlgeschlagener Speicherzuweisung passiert.
String-Zugriff
Auf die kanonische Darstellung kann mit zwei Makros zugegriffen werden: PyUnicode_Kind und PyUnicode_Data. PyUnicode_Kind gibt einen der Werte PyUnicode_WCHAR_KIND (0), PyUnicode_1BYTE_KIND (1), PyUnicode_2BYTE_KIND (2) oder PyUnicode_4BYTE_KIND (3) zurück. PyUnicode_DATA gibt den void-Zeiger auf die Daten zurück. Der Zugriff auf einzelne Zeichen sollte über PyUnicode_{READ|WRITE}[_CHAR] erfolgen.
- PyUnicode_READ(kind, data, index)
- PyUnicode_WRITE(kind, data, index, value)
- PyUnicode_READ_CHAR(unicode, index)
Alle diese Makros gehen davon aus, dass der String in kanonischer Form vorliegt; Aufrufer müssen dies durch den Aufruf von PyUnicode_READY sicherstellen.
Eine neue Funktion PyUnicode_AsUTF8 wird bereitgestellt, um auf die UTF-8-Darstellung zuzugreifen. Sie ist somit identisch mit dem vorhandenen _PyUnicode_AsString, das entfernt wird. Die Funktion berechnet die UTF-8-Darstellung bei ihrem ersten Aufruf. Da diese Darstellung Speicher verbraucht, bis das String-Objekt freigegeben wird, sollten Anwendungen, wo möglich, die vorhandene PyUnicode_AsUTF8String verwenden (die jedes Mal ein neues String-Objekt generiert). APIs, die einen String implizit in ein char* konvertieren (wie die ParseTuple-Funktionen), werden PyUnicode_AsUTF8 zur Berechnung einer Konvertierung verwenden.
Neue API
Dieser Abschnitt fasst die API-Ergänzungen zusammen.
Makros zum schreibgeschützten Zugriff auf die interne Darstellung eines Unicode-Objekts
- PyUnicode_IS_COMPACT_ASCII(o), PyUnicode_IS_COMPACT(o), PyUnicode_IS_READY(o)
- PyUnicode_GET_LENGTH(o)
- PyUnicode_KIND(o), PyUnicode_CHARACTER_SIZE(o), PyUnicode_MAX_CHAR_VALUE(o)
- PyUnicode_DATA(o), PyUnicode_1BYTE_DATA(o), PyUnicode_2BYTE_DATA(o), PyUnicode_4BYTE_DATA(o)
Zeichenzugriffs-Makros
- PyUnicode_READ(kind, data, index), PyUnicode_READ_CHAR(o, index)
- PyUnicode_WRITE(kind, data, index, value)
Andere Makros
- PyUnicode_READY(o)
- PyUnicode_CONVERT_BYTES(from_type, to_type, begin, end, to)
String-Erstellungsfunktionen
- PyUnicode_New(size, maxchar)
- PyUnicode_FromKindAndData(kind, data, size)
- PyUnicode_Substring(o, start, end)
Hilfsfunktionen für Zeichenzugriff
- PyUnicode_GetLength(o), PyUnicode_ReadChar(o, index), PyUnicode_WriteChar(o, index, character)
- PyUnicode_CopyCharacters(to, to_start, from, from_start, how_many)
- PyUnicode_FindChar(str, ch, start, end, direction)
Darstellungskonvertierung
- PyUnicode_AsUCS4(o, buffer, buflen)
- PyUnicode_AsUCS4Copy(o)
- PyUnicode_AsUnicodeAndSize(o, size_out)
- PyUnicode_AsUTF8(o)
- PyUnicode_AsUTF8AndSize(o, size_out)
UCS4-Hilfsfunktionen
- Py_UCS4_{strlen, strcpy, strcat, strncpy, strcmp, strncpy, strcmp, strncmp, strchr, strrchr}
Stabile ABI
Die folgenden Funktionen werden zur stabilen ABI (PEP 384) hinzugefügt, da sie unabhängig von der tatsächlichen Darstellung von Unicode-Objekten sind: PyUnicode_New, PyUnicode_Substring, PyUnicode_GetLength, PyUnicode_ReadChar, PyUnicode_WriteChar, PyUnicode_Find, PyUnicode_FindChar.
GDB Debugging Hooks
Tools/gdb/libpython.py enthält Debugging-Hooks, die Wissen über die Interna von CPython-Datentypen, einschließlich PyUnicodeObject-Instanzen, einbetten. Es wurde aktualisiert, um die Änderung zu berücksichtigen.
Veralterungen, Entfernungen und Inkompatibilitäten
Während die Py_UNICODE-Darstellung und die zugehörigen APIs mit diesem PEP veraltet sind, ist keine Entfernung der entsprechenden APIs geplant. Die APIs sollten mindestens fünf Jahre nach der Annahme des PEP verfügbar bleiben; bevor sie entfernt werden, sollten bestehende Erweiterungsmodule untersucht werden, um festzustellen, ob eine ausreichende Mehrheit des Open-Source-Codes auf PyPI auf die neue API portiert wurde. Eine vernünftige Motivation für die Verwendung der veralteten API auch in neuem Code ist für Code, der sowohl unter Python 2 als auch unter Python 3 funktionieren soll.
Die folgenden Makros und Funktionen sind veraltet:
- PyUnicode_FromUnicode
- PyUnicode_GET_SIZE, PyUnicode_GetSize, PyUnicode_GET_DATA_SIZE,
- PyUnicode_AS_UNICODE, PyUnicode_AsUnicode, PyUnicode_AsUnicodeAndSize
- PyUnicode_COPY, PyUnicode_FILL, PyUnicode_MATCH
- PyUnicode_Encode, PyUnicode_EncodeUTF7, PyUnicode_EncodeUTF8, PyUnicode_EncodeUTF16, PyUnicode_EncodeUTF32, PyUnicode_EncodeUnicodeEscape, PyUnicode_EncodeRawUnicodeEscape, PyUnicode_EncodeLatin1, PyUnicode_EncodeASCII, PyUnicode_EncodeCharmap, PyUnicode_TranslateCharmap, PyUnicode_EncodeMBCS, PyUnicode_EncodeDecimal, PyUnicode_TransformDecimalToASCII
- Py_UNICODE_{strlen, strcat, strcpy, strcmp, strchr, strrchr}
- PyUnicode_AsUnicodeCopy
- PyUnicode_GetMax
_PyUnicode_AsDefaultEncodedString wird entfernt. Es gab zuvor eine geliehene Referenz auf ein UTF-8-kodiertes Bytes-Objekt zurück. Da das Unicode-Objekt eine solche Referenz nicht mehr zwischenspeichern kann, ist die Implementierung ohne Speicherlecks nicht möglich. Es wird keine Veralterungsphase bereitgestellt, da es sich um eine API ausschließlich zur internen Verwendung handelte.
Erweiterungsmodule, die die Legacy-API verwenden, können unbeabsichtigt PyUnicode_READY aufrufen, indem sie eine API aufrufen, die erfordert, dass das Objekt bereit ist, und dann weiterhin auf den (jetzt ungültigen) Py_UNICODE-Zeiger zugreifen. Solcher Code wird mit diesem PEP fehlschlagen. Der Code war bereits in 3.2 fehlerhaft, da es keine explizite Garantie gab, dass das Ergebnis von PyUnicode_AS_UNICODE nach einem API-Aufruf gültig bleibt (aufgrund der Möglichkeit der String-Größenänderung). Module, die dieses Problem haben, müssen den Py_UNICODE-Zeiger nach API-Aufrufen erneut abrufen; dies wird in früheren Python-Versionen weiterhin korrekt funktionieren.
Diskussion
Mehrere Bedenken wurden bezüglich des hier vorgestellten Ansatzes geäußert:
Er macht die Implementierung komplexer. Das stimmt, wird aber angesichts der Vorteile als lohnenswert erachtet.
Die Py_UNICODE-Darstellung ist nicht sofort verfügbar, was Anwendungen, die sie anfordern, verlangsamt. Das stimmt zwar auch, aber Anwendungen, denen dieses Problem wichtig ist, können umgeschrieben werden, um die Datenrepräsentation zu verwenden.
Performance
Die Leistung dieses Patches muss sowohl für den Speicherverbrauch als auch für die Laufzeiteffizienz berücksichtigt werden. Beim Speicherverbrauch wird erwartet, dass Anwendungen mit vielen großen Strings eine Reduzierung des Speicherverbrauchs sehen. Bei kleinen Strings hängen die Effekte vom Zeigergrößen des Systems und der Größe des Py_UNICODE/wchar_t-Typs ab. Die folgende Tabelle zeigt dies für verschiedene Größen von kleinen ASCII- und Latin-1-Strings und Plattformen.
| String-Größe | Python 3.2 | Dieser PEP | ||||||
| 16-Bit wchar_t | 32-Bit wchar_t | ASCII | Latin-1 | |||||
| 32-Bit | 64-Bit | 32-Bit | 64-Bit | 32-Bit | 64-Bit | 32-Bit | 64-Bit | |
| 1 | 32 | 64 | 40 | 64 | 32 | 56 | 40 | 80 |
| 2 | 40 | 64 | 40 | 72 | 32 | 56 | 40 | 80 |
| 3 | 40 | 64 | 48 | 72 | 32 | 56 | 40 | 80 |
| 4 | 40 | 72 | 48 | 80 | 32 | 56 | 48 | 80 |
| 5 | 40 | 72 | 56 | 80 | 32 | 56 | 48 | 80 |
| 6 | 48 | 72 | 56 | 88 | 32 | 56 | 48 | 80 |
| 7 | 48 | 72 | 64 | 88 | 32 | 56 | 48 | 80 |
| 8 | 48 | 80 | 64 | 96 | 40 | 64 | 48 | 88 |
Die Laufzeiteffekte werden maßgeblich von der verwendeten API beeinflusst. Nach der Portierung relevanter Code-Teile auf die neue API sehen die iobench-, stringbench- und json-Benchmarks typischerweise Verlangsamungen von 1 % bis 30 %; für spezifische Benchmarks können Beschleunigungen auftreten, ebenso wie deutlich größere Verlangsamungen.
Bei tatsächlichen Messungen einer Django-Anwendung ([2]) konnten erhebliche Reduzierungen des Speicherverbrauchs festgestellt werden. Zum Beispiel reduzierte sich der Speicher für Unicode-Objekte auf 2216807 Bytes, gegenüber 6378540 Bytes für einen breiten Unicode-Build und 3694694 Bytes für einen schmalen Unicode-Build (alle auf einem 32-Bit-System). Diese Reduzierung ergab sich aus der Verbreitung von ASCII-Strings in dieser Anwendung; von 36.000 Strings (mit 1.310.000 Zeichen) waren 35713 ASCII-Strings (mit 1.300.000 Zeichen). Die Quellen dieser Strings wurden nicht weiter analysiert; viele davon stammen wahrscheinlich von Bezeichnern in der Bibliothek und String-Konstanten im Quellcode von Django.
Im Vergleich zu Python 2 müssen sowohl Unicode- als auch Byte-Strings berücksichtigt werden. In der Testanwendung hatten Unicode- und Byte-Strings zusammen eine Länge von 2.046.000 Einheiten (Bytes/Zeichen) in 2.x und 2.200.000 Einheiten in 3.x. Auf einem 32-Bit-System, wo der 2.x-Build 32-Bit wchar_t/Py_UNICODE verwendete, benötigte der 2.x-Test 3.620.000 Bytes und der 3.x-Build 3.340.000 Bytes. Diese Reduzierung in 3.x mit dem PEP im Vergleich zu 2.x tritt nur im Vergleich mit einem breiten Unicode-Build auf.
Portierungsrichtlinien
Nur ein kleiner Teil des C-Codes ist von diesem PEP betroffen, nämlich Code, der „in“ Unicode-Strings hineinschauen muss. Dieser Code muss nicht unbedingt auf diese API portiert werden, da die bestehende API weiterhin korrekt funktionieren wird. Insbesondere Module, die sowohl Python 2 als auch Python 3 unterstützen müssen, könnten zu kompliziert werden, wenn sie gleichzeitig diese neue API und die alte Unicode-API unterstützen.
Um Module auf die neue API zu portieren, versuchen Sie, die Verwendung dieser API-Elemente zu eliminieren:
- der Py_UNICODE-Typ,
- PyUnicode_AS_UNICODE und PyUnicode_AsUnicode,
- PyUnicode_GET_SIZE und PyUnicode_GetSize, und
- PyUnicode_FromUnicode.
Beim Iterieren über einen bestehenden String oder beim Betrachten spezifischer Zeichen verwenden Sie Indexierungsoperationen anstelle von Zeigerarithmetik; Indexierung funktioniert gut für PyUnicode_READ(_CHAR) und PyUnicode_WRITE. Verwenden Sie void* als Puffertyp für Zeichen, damit der Compiler ungültige Dereferenzierungsoperationen erkennt. Wenn Sie Zeigerarithmetik verwenden möchten (z. B. beim Konvertieren von bestehendem Code), verwenden Sie (unsigned) char* als Puffertyp und halten Sie die Elementgröße (1, 2 oder 4) in einer Variablen. Beachten Sie, dass (1<<(kind-1)) die Elementgröße bei gegebenem Puffer-Kind ergibt.
Beim Erstellen neuer Strings war es in Python üblich, mit einer heuristischen Puffergröße zu beginnen und dann zu vergrößern oder zu verkleinern, wenn die Heuristiken fehlschlugen. Mit diesem PEP ist dies nun weniger praktikabel, da Sie nicht nur eine Heuristik für die Länge des Strings, sondern auch für das maximale Zeichen benötigen.
Um Heuristiken zu vermeiden, müssen Sie den Input zweimal durchlaufen: einmal, um die Ausgabelänge und das maximale Zeichen zu bestimmen; dann den Zielstring mit PyUnicode_New zuweisen und den Input ein zweites Mal durchlaufen, um die endgültige Ausgabe zu erzeugen. Das mag zwar aufwendig klingen, kann aber tatsächlich günstiger sein, als das Ergebnis erneut kopieren zu müssen, wie im folgenden Ansatz.
Wenn Sie den heuristischen Weg wählen, vermeiden Sie die Zuweisung eines Strings, der für die Größenänderung vorgesehen ist, da die Größenänderung von Strings für ihre kanonische Darstellung nicht funktioniert. Weisen Sie stattdessen einen separaten Puffer zu, um die Zeichen zu sammeln, und erstellen Sie dann ein Unicode-Objekt daraus mit PyUnicode_FromKindAndData. Eine Möglichkeit ist die Verwendung von Py_UCS4 als Pufferelement, unter Annahme des schlimmsten Falls bei Zeichenordnungen. Dies ermöglicht Zeigerarithmetik, kann aber viel Speicher benötigen. Alternativ beginnen Sie mit einem 1-Byte-Puffer und erhöhen Sie die Elementgröße, wenn Sie auf größere Zeichen stoßen. In jedem Fall durchsucht PyUnicode_FromKindAndData den Puffer, um das maximale Zeichen zu überprüfen.
Für gängige Aufgaben ist direkter Zugriff auf die String-Darstellung möglicherweise nicht notwendig: PyUnicode_Find, PyUnicode_FindChar, PyUnicode_Ord und PyUnicode_CopyCharacters helfen bei der Analyse und Erstellung von String-Objekten, die auf Indizes statt auf Datenzeigern operieren.
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0393.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT