Python speichert Ganzzahlen im Bereich zwischen [-5, 256]
, sodass erwartet wird, dass auch Ganzzahlen in diesem Bereich identisch sind.
Was Sie sehen, ist der Python-Compiler, der identische Literale optimiert, wenn sie Teil desselben Textes sind.
Beim Eingeben der Python-Shell ist jede Zeile eine völlig andere Anweisung, die in einem anderen Moment analysiert wird.
>>> a = 257
>>> b = 257
>>> a is b
False
Wenn Sie jedoch denselben Code in eine Datei einfügen:
$ echo 'a = 257
> b = 257
> print a is b' > testing.py
$ python testing.py
True
Dies geschieht immer dann, wenn der Parser die Möglichkeit hat, zu analysieren, wo die Literale verwendet werden, beispielsweise beim Definieren einer Funktion im interaktiven Interpreter:
>>> def test():
... a = 257
... b = 257
... print a is b
...
>>> dis.dis(test)
2 0 LOAD_CONST 1 (257)
3 STORE_FAST 0 (a)
3 6 LOAD_CONST 1 (257)
9 STORE_FAST 1 (b)
4 12 LOAD_FAST 0 (a)
15 LOAD_FAST 1 (b)
18 COMPARE_OP 8 (is)
21 PRINT_ITEM
22 PRINT_NEWLINE
23 LOAD_CONST 0 (None)
26 RETURN_VALUE
>>> test()
True
>>> test.func_code.co_consts
(None, 257)
Beachten Sie, wie der kompilierte Code eine einzelne Konstante für die enthält 257
.
Zusammenfassend lässt sich sagen, dass der Python-Bytecode-Compiler keine massiven Optimierungen durchführen kann (wie Sprachen mit statischen Typen), aber mehr als Sie denken. Eines dieser Dinge ist, die Verwendung von Literalen zu analysieren und zu vermeiden, sie zu duplizieren.
Beachten Sie, dass dies nicht mit dem Cache zu tun hat, da dies auch für Floats funktioniert, die keinen Cache haben:
>>> a = 5.0
>>> b = 5.0
>>> a is b
False
>>> a = 5.0; b = 5.0
>>> a is b
True
Für komplexere Literale wie Tupel "funktioniert es nicht":
>>> a = (1,2)
>>> b = (1,2)
>>> a is b
False
>>> a = (1,2); b = (1,2)
>>> a is b
False
Aber die Literale im Tupel werden geteilt:
>>> a = (257, 258)
>>> b = (257, 258)
>>> a[0] is b[0]
False
>>> a[1] is b[1]
False
>>> a = (257, 258); b = (257, 258)
>>> a[0] is b[0]
True
>>> a[1] is b[1]
True
In Bezug darauf, warum Sie sehen, dass zwei PyInt_Object
erstellt werden, würde ich vermuten, dass dies getan wird, um einen wörtlichen Vergleich zu vermeiden. Zum Beispiel kann die Zahl 257
durch mehrere Literale ausgedrückt werden:
>>> 257
257
>>> 0x101
257
>>> 0b100000001
257
>>> 0o401
257
Der Parser hat zwei Möglichkeiten:
- Konvertieren Sie die Literale in eine gemeinsame Basis, bevor Sie die Ganzzahl erstellen, und prüfen Sie, ob die Literale gleichwertig sind. Erstellen Sie dann ein einzelnes ganzzahliges Objekt.
- Erstellen Sie die ganzzahligen Objekte und prüfen Sie, ob sie gleich sind. Wenn ja, behalten Sie nur einen einzigen Wert bei und weisen Sie ihn allen Literalen zu. Andernfalls müssen Sie bereits die Ganzzahlen zuweisen.
Wahrscheinlich verwendet der Python-Parser den zweiten Ansatz, der das Umschreiben des Konvertierungscodes vermeidet und auch einfacher zu erweitern ist (zum Beispiel funktioniert er auch mit Floats).
Beim Lesen der Python/ast.c
Datei werden alle Zahlen analysiert. Diese Funktion parsenumber
ruft PyOS_strtoul
auf, um den ganzzahligen Wert (für Ganzzahlen) zu erhalten, und ruft schließlich Folgendes auf PyLong_FromString
:
x = (long) PyOS_strtoul((char *)s, (char **)&end, 0);
if (x < 0 && errno == 0) {
return PyLong_FromString((char *)s,
(char **)0,
0);
}
Wie Sie hier sehen können, prüft der Parser nicht , ob er bereits eine Ganzzahl mit dem angegebenen Wert gefunden hat. Dies erklärt, warum Sie sehen, dass zwei int-Objekte erstellt werden, und dies bedeutet auch, dass meine Vermutung richtig war: Der Parser erstellt zuerst die Konstanten und erst danach wird der Bytecode optimiert, um dasselbe Objekt für gleiche Konstanten zu verwenden.
Der Code, der diese Prüfung durchführt, muss sich irgendwo in Python/compile.c
oder befinden Python/peephole.c
, da dies die Dateien sind, die den AST in Bytecode umwandeln.
Insbesondere compiler_add_o
scheint die Funktion diejenige zu sein, die dies tut. Es gibt diesen Kommentar in compiler_lambda
:
/* Make None the first constant, so the lambda can't have a
docstring. */
if (compiler_add_o(c, c->u->u_consts, Py_None) < 0)
return 0;
Es scheint also, als würde compiler_add_o
es verwendet, um Konstanten für Funktionen / Lambdas usw. einzufügen. Die compiler_add_o
Funktion speichert die Konstanten in einem dict
Objekt, und daraus folgt sofort, dass gleiche Konstanten in denselben Slot fallen, was zu einer einzelnen Konstante im endgültigen Bytecode führt.