PEP 626 – Präzise Zeilennummern für Debugging und andere Werkzeuge.
- Autor:
- Mark Shannon <mark at hotpy.org>
- BDFL-Delegate:
- Pablo Galindo <pablogsal at python.org>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 15. Juli 2020
- Python-Version:
- 3.10
- Post-History:
- 17. Juli 2020
Zusammenfassung
Python sollte garantieren, dass bei aktiviertem Tracing „Zeilen“-Tracing-Ereignisse für alle ausgeführten Codezeilen generiert werden und nur für Codezeilen, die ausgeführt werden.
Das Attribut f_lineno von Frame-Objekten sollte immer die erwartete Zeilennummer enthalten. Während der Frame-Ausführung ist die erwartete Zeilennummer die Zeilennummer des aktuell ausgeführten Quellcodes. Nachdem ein Frame entweder durch Rückkehr oder durch Auslösen einer Ausnahme abgeschlossen wurde, ist die erwartete Zeilennummer die Zeilennummer der letzten ausgeführten Quellcodezeile.
Eine Nebenwirkung der Sicherstellung korrekter Zeilennummern ist, dass einige Bytecodes als künstlich markiert werden müssen und keine aussagekräftige Zeilennummer haben. Um Tools zu unterstützen, wird ein neues Attribut co_lines hinzugefügt, das die Zuordnung von Bytecode zu Quelle beschreibt.
Motivation
Benutzer von sys.settrace und zugehörigen Werkzeugen sollten sich darauf verlassen können, dass für alle Codezeilen und nur für tatsächlichen Code Tracing-Ereignisse generiert werden. Sie sollten sich auch darauf verlassen können, dass die Zeilennummer in f_lineno korrekt ist.
Die aktuelle Implementierung tut dies größtenteils, versagt aber in einigen Fällen. Dies erfordert Workarounds in Werkzeugen und ist ein Ärgernis für alternative Python-Implementierungen.
Diese Garantie ist auch für Implementierer von CPython langfristig von Vorteil, da das aktuelle Verhalten nicht offensichtlich ist und einige seltsame Randfälle aufweist.
Begründung
Um zu garantieren, dass Zeilenereignisse wie erwartet generiert werden, kann das Attribut co_lnotab in seiner aktuellen Form nicht mehr die alleinige Quelle für Zeilennummerinformationen sein.
Anstatt zu versuchen, das Attribut co_lnotab zu reparieren, wird eine neue Methode co_lines() hinzugefügt, die einen Iterator über Bytecode-Offsets und Quellcodezeilen zurückgibt.
Die korrekte Annotation des Bytecodes, um genaue Zeilennummerinformationen zu ermöglichen, bedeutet, dass einige Bytecodes als künstlich markiert werden müssen und keine Zeilennummer haben dürfen.
Es muss darauf geachtet werden, bestehende Werkzeuge nicht zu brechen. Um den Bruch zu minimieren, wird das Attribut co_lnotab beibehalten, aber bei Bedarf verzögert generiert.
Spezifikation
Zeilenereignisse und das Attribut f_lineno sollten sich so verhalten, wie es ein erfahrener Python-Benutzer in allen Fällen erwarten würde.
Tracing
Tracing generiert Ereignisse für Aufrufe, Rückgaben, Ausnahmen, ausgeführte Quellcodezeilen und unter bestimmten Umständen ausgeführte Anweisungen.
Nur Zeilenereignisse werden von diesem PEP abgedeckt.
Wenn Tracing aktiviert ist, werden Zeilenereignisse generiert, wenn
- Eine neue Zeile Quellcode erreicht wird.
- Ein Rücksprung auftritt, auch wenn er zur selben Zeile springt, wie es bei Listen-Komprehensionen vorkommen kann.
Zusätzlich werden Zeilenereignisse nie für Quellcodezeilen generiert, die nicht ausgeführt werden.
Was gilt für das Tracing als Code
Alle Ausdrücke und Teile von Ausdrücken gelten als ausführbarer Code.
Im Allgemeinen gelten alle Anweisungen auch als ausführbarer Code. Wenn eine Anweisung jedoch über mehrere Zeilen verteilt ist, müssen wir überlegen, welche Teile einer Anweisung als ausführbarer Code gelten.
Anweisungen bestehen aus Schlüsselwörtern und Ausdrücken. Nicht alle Schlüsselwörter haben eine direkte Laufzeitwirkung, daher gelten nicht alle Schlüsselwörter als ausführbarer Code. Zum Beispiel ist else ein notwendiger Teil einer if-Anweisung, aber es gibt keine Laufzeitwirkung im Zusammenhang mit einem else.
Für Tracing-Zwecke werden die folgenden Schlüsselwörter nicht als ausführbarer Code betrachtet
del– Der zu löschende Ausdruck wird als ausführbarer Code behandelt.else– Keine Laufzeitwirkungfinally– Keine Laufzeitwirkungglobal– Rein deklarativnonlocal– Rein deklarativ
Alle anderen Schlüsselwörter gelten als ausführbarer Code.
Beispiel-Ereignissequenzen
In den folgenden Beispielen werden Ereignisse als „Name“, f_lineno-Paare aufgeführt.
Der Code
1. global x
2. x = a
generiert das folgende Ereignis
"line" 2
Der Code
1. try:
2. pass
3. finally:
4. pass
generiert die folgenden Ereignisse
"line" 1
"line" 2
"line" 4
Der Code
1. for (
2. x) in [1]:
3. pass
4. return
generiert die folgenden Ereignisse
"line" 2 # evaluate [1]
"line" 1 # for
"line" 2 # store to x
"line" 3 # pass
"line" 1 # for
"line" 4 # return
"return" 1
Das Attribut f_lineno
- Wenn ein Frame-Objekt erstellt wird, wird das Attribut
f_linenoauf die Zeile gesetzt, in der die Funktion oder Klasse definiert ist; dies ist die Zeile, in der das Schlüsselwortdefoderclasserscheint. Für Module wird es auf Null gesetzt. - Das Attribut
f_linenowird aktualisiert, um der Zeilennummer zu entsprechen, die gerade ausgeführt wird, auch wenn das Tracing deaktiviert ist und kein Ereignis generiert wird.
Die neue Methode co_lines() von Code-Objekten
Die Methode co_lines() gibt einen Iterator zurück, der Tupel von Werten liefert, die jeweils die Zeilennummer eines Bereichs von Bytecodes darstellen. Jedes Tupel besteht aus drei Werten
start– Der Offset (inklusiv) des Beginns des Bytecode-Bereichsend– Der Offset (exklusiv) des Endes des Bytecode-Bereichsline– Die Zeilennummer oderNone, wenn die Bytecodes im gegebenen Bereich keine Zeilennummer haben.
Die generierte Sequenz hat die folgenden Eigenschaften
- Der erste Bereich in der Sequenz hat
startvon0 - Die Bereiche
(start, end)sind nicht abnehmend und aufeinanderfolgend. Das heißt, für jedes Paar von Tupeln iststartdes zweiten gleichenddes ersten. - Kein Bereich ist rückwärts, d.h.
end >= startfür alle Tripel. - Der letzte Bereich in der Sequenz hat
endgleich der Größe des Bytecodes. lineist entweder eine positive ganze Zahl oderNone
Nullbreitenbereiche
Nullbreitenbereiche, d.h. Bereiche, bei denen start == end ist, sind zulässig. Nullbreitenbereiche werden für Zeilen verwendet, die im Quellcode vorhanden sind, aber vom Bytecode-Compiler eliminiert wurden.
Das Attribut co_linetable
Das Attribut co_linetable enthält die Zeilennummerinformationen. Das Format ist opak, nicht spezifiziert und kann ohne Vorankündigung geändert werden. Das Attribut ist nur öffentlich, um die Erstellung neuer Code-Objekte zu unterstützen.
Das Attribut co_lnotab
Historisch gesehen enthielt das Attribut co_lnotab eine Zuordnung von Bytecode-Offset zu Zeilennummer, unterstützt aber keine Bytecodes ohne Zeilennummer. Aus Kompatibilitätsgründen wird das co_lnotab-Bytes-Objekt bei Bedarf verzögert erstellt. Für Bereiche von Bytecodes ohne Zeilennummer wird die Zeilennummer des vorherigen Bytecode-Bereichs verwendet.
Werkzeuge, die die co_lnotab-Tabelle parsen, sollten so bald wie möglich zur neuen Methode co_lines() wechseln.
Abwärtskompatibilität
Das Attribut co_lnotab wird in 3.10 als veraltet markiert und in 3.12 entfernt.
Alle Werkzeuge, die das Attribut co_lnotab von Code-Objekten parsen, müssen vor der Veröffentlichung von 3.12 auf co_lines() umsteigen. Werkzeuge, die sys.settrace verwenden, sind nicht betroffen, außer in Fällen, in denen die empfangenen „Zeilen“-Ereignisse genauer sind.
Beispiele für Code, bei dem sich die Sequenz der Trace-Ereignisse ändert
In den folgenden Beispielen werden Ereignisse als „Name“, f_lineno-Paare aufgeführt.
Anweisung pass in einer if-Anweisung.
0. def spam(a):
1. if a:
2. eggs()
3. else:
4. pass
Wenn a True ist, dann ist die von Python 3.9 generierte Ereignissequenz
"line" 1
"line" 2
"line" 4
"return" 4
Ab 3.10 ist die Sequenz
"line" 1
"line" 2
"return" 2
Mehrere pass-Anweisungen.
0. def bar():
1. pass
2. pass
3. pass
Die von Python 3.9 generierte Ereignissequenz ist
"line" 3
"return" 3
Ab 3.10 ist die Sequenz
"line" 1
"line" 2
"line" 3
"return" 3
C API
Der Zugriff auf das Attribut f_lineno von Frame-Objekten über C-API-Funktionen ist unverändert. f_lineno kann von PyFrame_GetLineNumber gelesen werden. f_lineno kann nur über PyObject_SetAttr und ähnliche Funktionen gesetzt werden.
Der direkte Zugriff auf f_lineno über die zugrundeliegende Datenstruktur ist verboten.
Prozess-externe Debugger und Profiler
Prozess-externe Werkzeuge wie py-spy [1] können die C-API nicht verwenden und müssen die Zeilennummerntabelle selbst parsen. Obwohl das Format der Zeilennummerntabelle ohne Vorankündigung geändert werden kann, wird es während einer Veröffentlichung nicht geändert, es sei denn, es ist für eine Fehlerkorrektur unbedingt erforderlich.
Um den Aufwand für die Implementierung dieser Werkzeuge zu reduzieren, werden die folgende C-Struktur und Hilfsfunktionen bereitgestellt. Beachten Sie, dass diese Funktionen nicht Teil der C-API sind und in jeden Code, der sie verwenden muss, eingebunden werden müssen.
typedef struct addressrange {
int ar_start;
int ar_end;
int ar_line;
struct _opaque opaque;
} PyCodeAddressRange;
void PyLineTable_InitAddressRange(char *linetable, Py_ssize_t length, int firstlineno, PyCodeAddressRange *range);
int PyLineTable_NextAddressRange(PyCodeAddressRange *range);
int PyLineTable_PreviousAddressRange(PyCodeAddressRange *range);
PyLineTable_InitAddressRange initialisiert die PyCodeAddressRange-Struktur aus der Zeilennummerntabelle und der ersten Zeilennummer.
PyLineTable_NextAddressRange bewegt den Bereich zum nächsten Eintrag und gibt einen Wert ungleich Null zurück, wenn er gültig ist.
PyLineTable_PreviousAddressRange bewegt den Bereich zum vorherigen Eintrag und gibt einen Wert ungleich Null zurück, wenn er gültig ist.
Hinweis
Die Daten in linetable sind unveränderlich, aber ihre Lebensdauer hängt von ihrem Code-Objekt ab. Für einen zuverlässigen Betrieb sollte linetable in einen lokalen Puffer kopiert werden, bevor PyLineTable_InitAddressRange aufgerufen wird.
Obwohl diese Funktionen nicht Teil der C-API sind, werden sie von allen zukünftigen Versionen von CPython bereitgestellt. Die PyLineTable_-Funktionen rufen die C-API nicht auf und können daher sicher in jedes Werkzeug kopiert werden, das sie verwenden muss. Die PyCodeAddressRange-Struktur wird nicht geändert, aber die _opaque-Struktur ist nicht Teil der Spezifikation und kann sich ändern.
Hinweis
Die PyCodeAddressRange-Struktur hat sich gegenüber der ursprünglichen Version dieses PEP geändert, bei der die zusätzlichen Felder definiert waren, aber Änderungen unterlagen.
Zum Beispiel gibt der folgende Code alle Adressbereiche aus
void print_address_ranges(char *linetable, Py_ssize_t length, int firstlineno)
{
PyCodeAddressRange range;
PyLineTable_InitAddressRange(linetable, length, firstlineno, &range);
while (PyLineTable_NextAddressRange(&range)) {
printf("Bytecodes from %d (inclusive) to %d (exclusive) ",
range.start, range.end);
if (range.line < 0) {
/* line < 0 means no line number */
printf("have no line number\n");
}
else {
printf("have line number %d\n", range.line);
}
}
}
Performance-Implikationen
Im Allgemeinen sollte es keine Änderung der Leistung geben. Beim Tracing sollten Programme etwas schneller laufen, da das neue Tabellenformat mit Blick auf die Geschwindigkeit der Zeilennummernberechnung optimiert werden kann. Code mit langen Sequenzen von pass-Anweisungen wird wahrscheinlich etwas langsamer.
Referenzimplementierung
https://github.com/markshannon/cpython/tree/new-linetable-format-version-2
Urheberrecht
Dieses Dokument wird in die Public Domain oder unter die CC0-1.0-Universal-Lizenz gestellt, je nachdem, welche Lizenz permissiver ist.
Referenzen
Quelle: https://github.com/python/peps/blob/main/peps/pep-0626.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT