Après avoir plongé dans le code source de Python, je découvre qu'il maintient un tableau de PyInt_Object
S allant de int(-5)
à int(256)
(@ src/Objects/intobject.c)
Une petite expérience le prouve:
>>> a = 1
>>> b = 1
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False
Mais si j'exécute ces codes ensemble dans un fichier py (ou les joint avec des points-virgules), le résultat est différent:
>>> a = 257; b = 257; a is b
True
Je suis curieux de savoir pourquoi ils sont toujours le même objet, donc je fouille plus profondément dans l'arbre de syntaxe et le compilateur, j'ai trouvé une hiérarchie d'appels répertoriée ci-dessous:
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()
Ensuite, j'ai ajouté du code de débogage dans PyInt_FromLong
Et avant/après PyAST_FromNode
, Et j'ai exécuté un test.py:
a = 257
b = 257
print "id(a) = %d, id(b) = %d" % (id(a), id(b))
la sortie ressemble à:
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
Cela signifie que pendant la transformation cst
en ast
, deux PyInt_Object
Différents sont créés (en fait, ils sont exécutés dans la fonction ast_for_atom()
), mais ils sont fusionné plus tard.
J'ai du mal à comprendre la source dans PyAST_Compile
Et PyEval_EvalCode
, Donc je suis ici pour demander de l'aide, je serai reconnaissant si quelqu'un donne un indice?
Python met en cache les entiers de la plage [-5, 256]
, il est donc prévu que les entiers de cette plage soient également identiques.
Ce que vous voyez est le compilateur Python optimisant des littéraux identiques lorsqu'ils font partie du même texte.
Lorsque vous saisissez le Python Shell chaque ligne est une instruction complètement différente, analysée à un moment différent, donc:
>>> a = 257
>>> b = 257
>>> a is b
False
Mais si vous mettez le même code dans un fichier:
$ echo 'a = 257
> b = 257
> print a is b' > testing.py
$ python testing.py
True
Cela se produit chaque fois que l'analyseur a une chance d'analyser où les littéraux sont utilisés, par exemple lors de la définition d'une fonction dans l'interpréteur interactif:
>>> 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)
Notez comment le code compilé contient une seule constante pour le 257
.
En conclusion, le compilateur de bytecode Python n'est pas en mesure d'effectuer des optimisations massives (comme les langages de types statiques), mais il fait plus que vous ne le pensez. L'une de ces choses est d'analyser l'utilisation des littéraux et d'éviter les dupliquer.
Notez que cela n'a pas à voir avec le cache, car cela fonctionne également pour les flottants, qui n'ont pas de cache:
>>> a = 5.0
>>> b = 5.0
>>> a is b
False
>>> a = 5.0; b = 5.0
>>> a is b
True
Pour les littéraux plus complexes, comme les tuples, cela "ne fonctionne pas":
>>> a = (1,2)
>>> b = (1,2)
>>> a is b
False
>>> a = (1,2); b = (1,2)
>>> a is b
False
Mais les littéraux à l'intérieur du Tuple sont partagés:
>>> 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
En ce qui concerne la raison pour laquelle vous voyez que deux PyInt_Object
Sont créés, je devine que cela est fait pour éviter la comparaison littérale. par exemple, le nombre 257
peut être exprimé par plusieurs littéraux:
>>> 257
257
>>> 0x101
257
>>> 0b100000001
257
>>> 0o401
257
L'analyseur a deux choix:
Probablement le Python utilise la deuxième approche, ce qui évite de réécrire le code de conversion et il est également plus facile à étendre (par exemple, il fonctionne également avec les flottants).
En lisant le fichier Python/ast.c
, La fonction qui analyse tous les nombres est parsenumber
, qui appelle PyOS_strtoul
Pour obtenir la valeur entière (pour les entiers) et appelle finalement PyLong_FromString
:
x = (long) PyOS_strtoul((char *)s, (char **)&end, 0);
if (x < 0 && errno == 0) {
return PyLong_FromString((char *)s,
(char **)0,
0);
}
Comme vous pouvez le voir ici, l'analyseur ne pas vérifie s'il a déjà trouvé un entier avec la valeur donnée et cela explique pourquoi vous voyez que deux objets int sont créés, et cela signifie également que ma supposition était correct: l'analyseur crée d'abord les constantes et optimise ensuite le bytecode pour utiliser le même objet pour des constantes égales.
Le code qui effectue cette vérification doit être quelque part dans Python/compile.c
Ou Python/peephole.c
, Car ce sont les fichiers qui transforment le AST en bytecode.
En particulier, la fonction compiler_add_o
Semble être celle qui le fait. Il y a ce commentaire dans 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;
Il semble donc que compiler_add_o
Soit utilisé pour insérer des constantes pour les fonctions/lambdas etc. La fonction compiler_add_o
Stocke les constantes dans un objet dict
, et de là découle immédiatement que des constantes égales tombera dans le même emplacement, résultant en une seule constante dans le bytecode final.