98

Python のソース コードを調べてみると、から(@src/Objects/intobject.c)PyInt_Objectまでの s の配列が保持されていることがわかりました。int(-5)int(256)

ちょっとした実験がそれを証明しています:

>>> 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次に、 inと before/after にデバッグ コードを追加し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

これは、csttoast変換中に 2 つPyInt_Objectの異なる が作成される (実際にはast_for_atom()関数内で実行される) が、後でマージされることを意味します。

PyAST_Compileとのソースを理解するのは難しいと思うPyEval_EvalCodeので、助けを求めるためにここにいます。

4

1 に答える 1

120

Python は範囲内の整数をキャッシュする[-5, 256]ため、通常、その範囲内の整数は常に同じではありません。

257 に表示されるのは、同じコード オブジェクトでコンパイルされたときに、同一のリテラルを最適化する Python コンパイラです。

Python シェルで入力すると、各行は完全に異なるステートメントであり、個別に解析およびコンパイルされるため、次のようになります。

>>> 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

(定数フォールディングとピープホール オプティマイザーは、バグ修正バージョン間でも動作を変更できることに注意してください。そのため、どの例が返さTrueれるかFalse、基本的に任意であり、将来変更される予定です)。


PyInt_Object2つが作成されているのを見る理由については、これは文字通りの比較を避けるために行われていると思います。たとえば、数値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);
    }

ここでわかるように、パーサーは、指定された値の整数が既に見つかったかどうかをチェックしませ。これは、2 つの int オブジェクトが作成される理由を説明しています。これは、私の推測が正しかったことも意味します。パーサーは最初に定数を作成します。その後、等しい定数に同じオブジェクトを使用するようにバイトコードを最適化します。

これらは AST をバイトコードに変換するファイルであるため、このチェックを行うコードはPython/compile.cまたはのどこかにある必要があります。Python/peephole.c

特に、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オブジェクトに格納します。これにより、等しい定数が同じスロットに収まり、最終的なバイトコードで単一の定数になります。

于 2013-03-02T07:54:41.213 に答える