Python buforuje liczby całkowite w zakresie [-5, 256]
, więc oczekuje się, że liczby całkowite w tym zakresie są również identyczne.
To, co widzisz, to kompilator Pythona optymalizujący identyczne literały, gdy są częścią tego samego tekstu.
Podczas pisania w powłoce Pythona każda linia jest zupełnie inną instrukcją, analizowaną w innym momencie, a więc:
>>> a = 257
>>> b = 257
>>> a is b
False
Ale jeśli umieścisz ten sam kod w pliku:
$ echo 'a = 257
> b = 257
> print a is b' > testing.py
$ python testing.py
True
Dzieje się tak za każdym razem, gdy parser ma szansę przeanalizować, gdzie są używane literały, na przykład podczas definiowania funkcji w interaktywnym interpretatorze:
>>> 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)
Zwróć uwagę, jak skompilowany kod zawiera jedną stałą dla 257
.
Podsumowując, kompilator kodu bajtowego Pythona nie jest w stanie wykonywać masowych optymalizacji (takich jak języki statyczne), ale robi więcej niż myślisz. Jedną z tych rzeczy jest analiza użycia literałów i unikanie ich dublowania.
Zauważ, że nie ma to nic wspólnego z pamięcią podręczną, ponieważ działa również dla pływaków, które nie mają pamięci podręcznej:
>>> a = 5.0
>>> b = 5.0
>>> a is b
False
>>> a = 5.0; b = 5.0
>>> a is b
True
W przypadku bardziej złożonych literałów, takich jak krotki, „nie działa”:
>>> a = (1,2)
>>> b = (1,2)
>>> a is b
False
>>> a = (1,2); b = (1,2)
>>> a is b
False
Ale dosłowne wewnątrz krotki są udostępniane:
>>> 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
(Zauważ, że ciągłe zwijanie i optymalizator wizjera mogą zmieniać zachowanie nawet między wersjami poprawek, więc które przykłady powracają True
lub False
są w zasadzie arbitralne i zmieniają się w przyszłości).
Jeśli chodzi o to, dlaczego widzisz, że dwa PyInt_Object
są tworzone, przypuszczam, że ma to na celu uniknięcie dosłownego porównania. na przykład liczbę 257
można wyrazić wieloma literałami:
>>> 257
257
>>> 0x101
257
>>> 0b100000001
257
>>> 0o401
257
Parser ma dwie możliwości:
- Przekonwertuj literały na jakąś wspólną podstawę przed utworzeniem liczby całkowitej i sprawdź, czy literały są równoważne. następnie utwórz pojedynczy obiekt typu integer.
- Utwórz obiekty typu integer i zobacz, czy są równe. Jeśli tak, zachowaj tylko jedną wartość i przypisz ją do wszystkich literałów, w przeciwnym razie masz już liczby całkowite do przypisania.
Prawdopodobnie parser Pythona używa drugiego podejścia, które pozwala uniknąć przepisywania kodu konwersji, a także jest łatwiejsze do rozszerzenia (na przykład działa również z liczbami zmiennoprzecinkowymi).
Czytając Python/ast.c
plik, funkcja analizująca wszystkie liczby to parsenumber
, która wywołuje PyOS_strtoul
uzyskanie wartości całkowitej (dla liczb całkowitych) i ostatecznie wywołuje PyLong_FromString
:
x = (long) PyOS_strtoul((char *)s, (char **)&end, 0);
if (x < 0 && errno == 0) {
return PyLong_FromString((char *)s,
(char **)0,
0);
}
Jak widać, parser tego nie robi sprawdza, czy już znalazł liczbę całkowitą o podanej wartości, więc to wyjaśnia, dlaczego widzisz, że zostały utworzone dwa obiekty int, a to również oznacza, że moje przypuszczenie było poprawne: parser najpierw tworzy stałe i dopiero później optymalizuje kod bajtowy, aby używać tego samego obiektu dla równych stałych.
Kod, który wykonuje to sprawdzenie, musi znajdować się gdzieś w Python/compile.c
lubPython/peephole.c
, ponieważ są to pliki, które przekształcają AST na kod bajtowy.
W szczególności compiler_add_o
funkcja wydaje się być tą, która to robi. Jest taki komentarz w 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;
Wygląda więc na to, że compiler_add_o
jest używany do wstawiania stałych dla funkcji / lambd itp. compiler_add_o
Funkcja przechowuje stałe do dict
obiektu, a stąd natychmiast wynika, że równe stałe będą znajdować się w tym samym gnieździe, dając pojedynczą stałą w końcowym kodzie bajtowym.