Pythonのソースコードに飛び込んだ後、int(-5)
からint(256)
までの_PyInt_Object
_ sの配列を維持していることがわかりました(@ src/Objects/intobject.c)
小さな実験でそれが証明されます:
_>>> a = 1
>>> b = 1
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False
_
しかし、これらのコードをpyファイルで一緒に実行する(またはセミコロンで結合する)と、結果は異なります。
_>>> a = 257; b = 257; a is b
True
_
それらがまだ同じオブジェクトである理由に興味があるので、構文ツリーとコンパイラーをさらに掘り下げて、以下の呼び出し階層を考え出しました。
_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()
_
次に、_PyInt_FromLong
_および_PyAST_FromNode
_の前後にデバッグコードを追加し、test.pyを実行しました。
_a = 257
b = 257
print "id(a) = %d, id(b) = %d" % (id(a), id(b))
_
出力は次のようになります。
_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
_
つまり、cst
からast
への変換中に、2つの異なる_PyInt_Object
_ sが作成されます(実際にはast_for_atom()
関数で実行されます)が、後でマージされます。
_PyAST_Compile
_と_PyEval_EvalCode
_でソースを理解するのは難しいので、助けを求めるためにここにいます。誰かがヒントを教えてくれれば感謝しますか?
Pythonは [-5, 256]
の範囲の整数をキャッシュするため、その範囲の整数も同じであることが期待されます。
表示されるのは、Pythonコンパイラが同じテキストの一部である場合に同一のリテラルを最適化するコンパイラです。
Python Shellと入力すると、各行は完全に異なるステートメントであり、別の瞬間に解析されるため、次のようになります。
>>> a = 257
>>> b = 257
>>> a is b
False
しかし、同じコードをファイルに入れた場合:
$ echo 'a = 257
> b = 257
> print a is b' > testing.py
$ python testing.py
True
これは、パーサーがリテラルが使用されている場所を分析する機会があるときはいつでも発生します。たとえば、対話型インタープリターで関数を定義するときなどです。
>>> 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)
コンパイルされたコードに257
の単一の定数が含まれていることに注意してください。
結論として、Pythonバイトコードコンパイラは(静的型言語のように)大規模な最適化を実行することはできませんが、想像以上に機能します。これらのことの1つは、リテラルの使用を分析して回避することですそれらを複製します。
キャッシュを持たないフロートに対しても機能するため、これはキャッシュとは関係ありません。
>>> a = 5.0
>>> b = 5.0
>>> a is b
False
>>> a = 5.0; b = 5.0
>>> a is b
True
タプルのようなより複雑なリテラルの場合、「機能しません」:
>>> a = (1,2)
>>> b = (1,2)
>>> a is b
False
>>> a = (1,2); b = (1,2)
>>> a is b
False
しかし、タプル内のリテラルは共有されます:
>>> 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
2つのPyInt_Object
が作成された理由について、リテラル比較を回避するためにguessしたと思います。たとえば、数字257
は複数のリテラルで表すことができます。
>>> 257
257
>>> 0x101
257
>>> 0b100000001
257
>>> 0o401
257
パーサーには2つの選択肢があります。
おそらくPythonパーサーは2番目のアプローチを使用します。これにより、変換コードを書き直す必要がなくなり、拡張も容易になります(たとえば、フロートでも機能します)。
Python/ast.c
ファイルを読み取ると、すべての数値を解析する関数はparsenumber
で、PyOS_strtoul
を呼び出して整数値(整数の場合)を取得し、最終的にPyLong_FromString
を呼び出します。
x = (long) PyOS_strtoul((char *)s, (char **)&end, 0);
if (x < 0 && errno == 0) {
return PyLong_FromString((char *)s,
(char **)0,
0);
}
ここに見られるように、パーサーはnot指定された値の整数をすでに見つけたかどうかをチェックします。これにより、2つのintオブジェクトが作成されることがわかります。これはまた、私の推測は正しかった:パーサーは最初に定数を作成し、その後になって初めて、等しいオブジェクトに対して同じオブジェクトを使用するようにバイトコードを最適化します。
このチェックを行うコードは、Python/compile.c
またはPython/peephole.c
のどこかにある必要があります。これらはAST=をバイトコードに変換するファイルであるためです。
特に、compiler_add_o
関数はそれを行う関数のようです。このコメントは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;
したがって、compiler_add_o
は関数/ラムダなどの定数を挿入するために使用されているようです。compiler_add_o
関数は定数をdict
オブジェクトに格納し、これからすぐに等しい定数が同じスロットで、最終的なバイトコードに単一の定数をもたらします。