Po zanurzeniu się w kodzie źródłowym Pythona stwierdzam, że utrzymuje on tablicę PyInt_Object
s odint(-5)
do int(256)
(@ src / Objects / intobject.c)
Mały eksperyment to potwierdza:
>>> a = 1
>>> b = 1
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False
Ale jeśli uruchomię ten kod razem w pliku py (lub połączę je średnikami), wynik będzie inny:
>>> a = 257; b = 257; a is b
True
Jestem ciekawy, dlaczego wciąż są tym samym obiektem, więc zagłębiłem się w drzewo składni i kompilator, znalazłem hierarchię wywołań wymienioną poniżej:
PyRun_FileExFlags()
mod = PyParser_ASTFromFile()
node *n = PyParser_ParseFileFlagsEx() //source to cst
parsetoke()
ps = PyParser_New()
for (;;)
PyTokenizer_Get()
PyParser_AddToken(ps, ...)
mod = PyAST_FromNode(n, ...) //cst to ast
run_mod(mod, ...)
co = PyAST_Compile(mod, ...) //ast to CFG
PyFuture_FromAST()
PySymtable_Build()
co = compiler_mod()
PyEval_EvalCode(co, ...)
PyEval_EvalCodeEx()
Następnie dodałem kod debugowania w PyInt_FromLong
i przed / po PyAST_FromNode
oraz wykonałem test.py:
a = 257
b = 257
print "id(a) = %d, id(b) = %d" % (id(a), id(b))
wyjście wygląda następująco:
DEBUG: before PyAST_FromNode
name = a
ival = 257, id = 176046536
name = b
ival = 257, id = 176046752
name = a
name = b
DEBUG: after PyAST_FromNode
run_mod
PyAST_Compile ok
id(a) = 176046536, id(b) = 176046536
Eval ok
Oznacza to, że w okresie cst
doast
przekształcić dwa różne PyInt_Object
s tworzone są (w rzeczywistości jest to wykonywane w ast_for_atom()
funkcji), ale są one później połączyły.
Trudno mi zrozumieć źródło w PyAST_Compile
i PyEval_EvalCode
, więc jestem tutaj, aby poprosić o pomoc, będę wdzięczny, jeśli ktoś daje wskazówkę?
źródło
Odpowiedzi:
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
lubFalse
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:
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 toparsenumber
, która wywołujePyOS_strtoul
uzyskanie wartości całkowitej (dla liczb całkowitych) i ostatecznie wywołujePyLong_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 wcompiler_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 dodict
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.źródło
False
wykonywaniaa = 5.0; b = 5.0; print (a is b)
zarówno z Py2 i py3 na win7True
(właśnie sprawdzone ponownie). Optymalizacje nie są niezawodne, ponieważ są tylko szczegółem implementacji, więc nie unieważnia tego, co chciałem zawrzeć w mojej odpowiedzi.compile('a=5.0;b=5.0', '<stdin>', 'exec')).co_consts
Pokazuje również , że istnieje tylko5.0
stała (w pythonie3.3 w systemie Linux).